Configuration Management
In a microservices architecture, managing configuration across dozens or hundreds of services is a significant challenge. This chapter covers patterns and tools for centralized, dynamic configuration.Learning Objectives:
- Implement centralized configuration management
- Set up dynamic configuration with hot reload
- Design feature flags for progressive rollouts
- Manage environment-specific configurations
- Handle secrets securely across services
The Configuration Challenge
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONFIGURATION CHALLENGES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT CENTRALIZED CONFIG: │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │.env file │ │ │ │.env file │ │ │ │.env file │ │ │
│ │ │DB_HOST=..│ │ │ │DB_HOST=..│ │ │ │DB_HOST=..│ │ │
│ │ │API_KEY=..│ │ │ │API_KEY=..│ │ │ │API_KEY=..│ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ⚠️ Problems: │
│ • Configuration scattered across services │
│ • Hard to update consistently │
│ • Requires redeployment for changes │
│ • Secrets in plain text files │
│ • No audit trail │
│ │
│ ══════════════════════════════════════════════════════════════════════════ │
│ │
│ WITH CENTRALIZED CONFIG: │
│ │
│ ┌────────────────────────┐ │
│ │ Config Server │ │
│ │ ┌────────────────┐ │ │
│ │ │ Consul / etcd │ │ │
│ │ │ Vault / AWS SM │ │ │
│ │ └────────────────┘ │ │
│ └───────────┬────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ │
│ │ (watches) │ │ (watches) │ │ (watches) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ✅ Benefits: │
│ • Single source of truth │
│ • Hot reload without redeployment │
│ • Encrypted secrets │
│ • Audit logging │
│ • Environment-specific overrides │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The 12-Factor App Configuration
Factor III: Config
Store config in the environment, not in code:Copy
// ❌ Bad: Hardcoded configuration
const config = {
database: {
host: 'localhost',
port: 5432,
password: 'secretpassword' // Never do this!
},
api: {
timeout: 5000,
retries: 3
}
};
// ✅ Good: Environment-based configuration
const config = {
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 5432,
password: process.env.DB_PASSWORD // Must be set externally
},
api: {
timeout: parseInt(process.env.API_TIMEOUT, 10) || 5000,
retries: parseInt(process.env.API_RETRIES, 10) || 3
}
};
Configuration Hierarchy
Copy
// config/index.js - Hierarchical configuration with validation
const convict = require('convict');
const path = require('path');
const config = convict({
env: {
doc: 'The application environment',
format: ['production', 'staging', 'development', 'test'],
default: 'development',
env: 'NODE_ENV'
},
server: {
port: {
doc: 'The port to bind to',
format: 'port',
default: 3000,
env: 'PORT'
},
host: {
doc: 'The host to bind to',
format: 'ipaddress',
default: '0.0.0.0',
env: 'HOST'
}
},
database: {
host: {
doc: 'Database host',
format: String,
default: 'localhost',
env: 'DB_HOST'
},
port: {
doc: 'Database port',
format: 'port',
default: 5432,
env: 'DB_PORT'
},
name: {
doc: 'Database name',
format: String,
default: 'myapp',
env: 'DB_NAME'
},
username: {
doc: 'Database username',
format: String,
default: '',
env: 'DB_USERNAME',
sensitive: true
},
password: {
doc: 'Database password',
format: String,
default: '',
env: 'DB_PASSWORD',
sensitive: true
},
pool: {
min: {
doc: 'Minimum pool size',
format: 'nat',
default: 2,
env: 'DB_POOL_MIN'
},
max: {
doc: 'Maximum pool size',
format: 'nat',
default: 10,
env: 'DB_POOL_MAX'
}
}
},
redis: {
host: {
doc: 'Redis host',
format: String,
default: 'localhost',
env: 'REDIS_HOST'
},
port: {
doc: 'Redis port',
format: 'port',
default: 6379,
env: 'REDIS_PORT'
},
password: {
doc: 'Redis password',
format: String,
default: '',
env: 'REDIS_PASSWORD',
sensitive: true
}
},
services: {
payment: {
url: {
doc: 'Payment service URL',
format: 'url',
default: 'http://payment-service:3000',
env: 'PAYMENT_SERVICE_URL'
},
timeout: {
doc: 'Payment service timeout (ms)',
format: 'nat',
default: 5000,
env: 'PAYMENT_SERVICE_TIMEOUT'
}
},
inventory: {
url: {
doc: 'Inventory service URL',
format: 'url',
default: 'http://inventory-service:3000',
env: 'INVENTORY_SERVICE_URL'
}
}
},
features: {
newCheckout: {
doc: 'Enable new checkout flow',
format: Boolean,
default: false,
env: 'FEATURE_NEW_CHECKOUT'
},
darkMode: {
doc: 'Enable dark mode',
format: Boolean,
default: true,
env: 'FEATURE_DARK_MODE'
}
},
logging: {
level: {
doc: 'Log level',
format: ['error', 'warn', 'info', 'debug'],
default: 'info',
env: 'LOG_LEVEL'
}
}
});
// Load environment-specific config
const env = config.get('env');
const configPath = path.join(__dirname, `${env}.json`);
try {
config.loadFile(configPath);
} catch (e) {
console.log(`No config file found for ${env}, using defaults and env vars`);
}
// Validate configuration
config.validate({ allowed: 'strict' });
module.exports = config;
Consul for Configuration
Setup and Connection
Copy
// config/consul-config.js
const Consul = require('consul');
const EventEmitter = require('events');
class ConsulConfig extends EventEmitter {
constructor(options = {}) {
super();
this.consul = new Consul({
host: process.env.CONSUL_HOST || 'localhost',
port: process.env.CONSUL_PORT || 8500,
promisify: true
});
this.serviceName = options.serviceName || process.env.SERVICE_NAME;
this.environment = options.environment || process.env.NODE_ENV || 'development';
this.prefix = `config/${this.environment}`;
this.config = {};
this.watchers = new Map();
}
async load() {
// Load global config
const globalConfig = await this.getPrefix(`${this.prefix}/global`);
// Load service-specific config (overrides global)
const serviceConfig = await this.getPrefix(`${this.prefix}/${this.serviceName}`);
// Merge configs
this.config = this.deepMerge(globalConfig, serviceConfig);
console.log(`Loaded configuration for ${this.serviceName} in ${this.environment}`);
return this.config;
}
async getPrefix(prefix) {
try {
const result = await this.consul.kv.get({
key: prefix,
recurse: true
});
if (!result) return {};
const config = {};
for (const item of result) {
const key = item.Key.replace(`${prefix}/`, '');
const value = this.parseValue(item.Value);
this.setNestedValue(config, key, value);
}
return config;
} catch (error) {
console.error(`Failed to load config from ${prefix}:`, error.message);
return {};
}
}
parseValue(value) {
if (!value) return null;
try {
return JSON.parse(value);
} catch {
// Not JSON, return as string
return value;
}
}
setNestedValue(obj, path, value) {
const keys = path.split('/');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current)) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] instanceof Object && key in target) {
result[key] = this.deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
get(path, defaultValue = undefined) {
const keys = path.split('.');
let value = this.config;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return defaultValue;
}
}
return value;
}
// Watch for configuration changes
watch(key, callback) {
const fullKey = `${this.prefix}/${this.serviceName}/${key}`;
const watcher = this.consul.watch({
method: this.consul.kv.get,
options: { key: fullKey }
});
watcher.on('change', (data) => {
const newValue = data ? this.parseValue(data.Value) : null;
const oldValue = this.get(key.replace(/\//g, '.'));
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
// Update local config
this.setNestedValue(this.config, key, newValue);
// Emit change event
this.emit('change', { key, oldValue, newValue });
callback(newValue, oldValue);
}
});
watcher.on('error', (err) => {
console.error(`Watch error for ${key}:`, err);
});
this.watchers.set(key, watcher);
return watcher;
}
// Set configuration value
async set(key, value) {
const fullKey = `${this.prefix}/${this.serviceName}/${key}`;
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
await this.consul.kv.set(fullKey, stringValue);
this.setNestedValue(this.config, key, value);
}
// Close all watchers
close() {
for (const watcher of this.watchers.values()) {
watcher.end();
}
this.watchers.clear();
}
}
module.exports = ConsulConfig;
Using Consul Config in Services
Copy
// app.js
const express = require('express');
const ConsulConfig = require('./config/consul-config');
const app = express();
const config = new ConsulConfig({ serviceName: 'order-service' });
async function startServer() {
// Load initial configuration
await config.load();
// Watch for configuration changes
config.watch('database', (newValue, oldValue) => {
console.log('Database config changed:', { oldValue, newValue });
// Reconnect to database with new config
reconnectDatabase(newValue);
});
config.watch('features/rateLimit', (newValue) => {
console.log('Rate limit changed to:', newValue);
updateRateLimiter(newValue);
});
// Listen for any config changes
config.on('change', ({ key, oldValue, newValue }) => {
console.log(`Config changed: ${key}`, { oldValue, newValue });
});
// Use configuration
const port = config.get('server.port', 3000);
const dbConfig = config.get('database');
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
config: {
environment: config.environment,
features: config.get('features')
}
});
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
startServer().catch(console.error);
Feature Flags
Feature Flag System
Copy
// features/feature-flags.js
const ConsulConfig = require('./config/consul-config');
const crypto = require('crypto');
class FeatureFlags {
constructor(options = {}) {
this.config = options.config || new ConsulConfig({ serviceName: 'features' });
this.flags = new Map();
this.overrides = new Map(); // For testing
}
async initialize() {
await this.config.load();
// Watch for feature flag changes
this.config.watch('flags', (newFlags) => {
console.log('Feature flags updated:', newFlags);
this.updateFlags(newFlags);
});
this.updateFlags(this.config.get('flags', {}));
}
updateFlags(flags) {
this.flags.clear();
for (const [name, config] of Object.entries(flags)) {
this.flags.set(name, this.parseFlag(config));
}
}
parseFlag(config) {
if (typeof config === 'boolean') {
return { enabled: config, type: 'boolean' };
}
return config;
}
// Check if feature is enabled
isEnabled(flagName, context = {}) {
// Check overrides first (for testing)
if (this.overrides.has(flagName)) {
return this.overrides.get(flagName);
}
const flag = this.flags.get(flagName);
if (!flag) return false;
switch (flag.type) {
case 'boolean':
return flag.enabled;
case 'percentage':
return this.checkPercentage(flag, context);
case 'userList':
return this.checkUserList(flag, context);
case 'gradualRollout':
return this.checkGradualRollout(flag, context);
case 'userAttribute':
return this.checkUserAttribute(flag, context);
default:
return flag.enabled || false;
}
}
// Percentage-based rollout
checkPercentage(flag, context) {
const userId = context.userId || context.sessionId || 'anonymous';
const hash = crypto.createHash('md5').update(userId + flag.name).digest('hex');
const percentage = parseInt(hash.substring(0, 8), 16) % 100;
return percentage < flag.percentage;
}
// User allowlist
checkUserList(flag, context) {
if (!context.userId) return false;
return flag.users.includes(context.userId);
}
// Gradual rollout based on time
checkGradualRollout(flag, context) {
const now = Date.now();
const start = new Date(flag.startDate).getTime();
const end = new Date(flag.endDate).getTime();
if (now < start) return false;
if (now >= end) return true;
const progress = (now - start) / (end - start);
return this.checkPercentage({ ...flag, percentage: progress * 100 }, context);
}
// User attribute matching
checkUserAttribute(flag, context) {
const userValue = context[flag.attribute];
if (!userValue) return false;
switch (flag.operator) {
case 'equals':
return userValue === flag.value;
case 'contains':
return userValue.includes(flag.value);
case 'in':
return flag.values.includes(userValue);
case 'regex':
return new RegExp(flag.pattern).test(userValue);
default:
return false;
}
}
// Get flag value (for non-boolean flags)
getValue(flagName, defaultValue, context = {}) {
const flag = this.flags.get(flagName);
if (!flag) return defaultValue;
if (!this.isEnabled(flagName, context)) {
return defaultValue;
}
return flag.value !== undefined ? flag.value : defaultValue;
}
// Set override for testing
setOverride(flagName, value) {
this.overrides.set(flagName, value);
}
clearOverrides() {
this.overrides.clear();
}
// Get all flags for debugging
getAllFlags() {
const result = {};
for (const [name, config] of this.flags) {
result[name] = config;
}
return result;
}
}
// Singleton instance
let instance = null;
module.exports = {
FeatureFlags,
async getFeatureFlags() {
if (!instance) {
instance = new FeatureFlags();
await instance.initialize();
}
return instance;
}
};
Feature Flag Configuration in Consul
Copy
// Stored in Consul at: config/production/features/flags
{
"newCheckoutFlow": {
"type": "percentage",
"name": "newCheckoutFlow",
"percentage": 25,
"description": "New streamlined checkout experience"
},
"betaFeatures": {
"type": "userList",
"name": "betaFeatures",
"users": ["user-123", "user-456", "user-789"],
"description": "Beta features for selected users"
},
"darkMode": {
"type": "boolean",
"enabled": true,
"description": "Dark mode UI"
},
"newRecommendationEngine": {
"type": "gradualRollout",
"name": "newRecommendationEngine",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-01-15T00:00:00Z",
"description": "Gradual rollout of new ML recommendations"
},
"premiumFeatures": {
"type": "userAttribute",
"attribute": "subscriptionTier",
"operator": "in",
"values": ["premium", "enterprise"],
"description": "Features for premium subscribers"
},
"experimentalApi": {
"type": "percentage",
"name": "experimentalApi",
"percentage": 10,
"value": {
"apiVersion": "v2",
"timeout": 10000
},
"description": "Test new API version"
}
}
Using Feature Flags in Routes
Copy
// routes/checkout.js
const express = require('express');
const router = express.Router();
const { getFeatureFlags } = require('../features/feature-flags');
router.post('/checkout', async (req, res) => {
const featureFlags = await getFeatureFlags();
const context = {
userId: req.user.id,
subscriptionTier: req.user.subscriptionTier,
country: req.user.country
};
if (featureFlags.isEnabled('newCheckoutFlow', context)) {
// New checkout flow
return handleNewCheckout(req, res);
}
// Legacy checkout flow
return handleLegacyCheckout(req, res);
});
// Feature flag middleware
const featureFlagMiddleware = (flagName, options = {}) => {
return async (req, res, next) => {
const featureFlags = await getFeatureFlags();
const context = {
userId: req.user?.id,
sessionId: req.sessionID,
...req.user
};
const isEnabled = featureFlags.isEnabled(flagName, context);
req.featureFlags = req.featureFlags || {};
req.featureFlags[flagName] = isEnabled;
if (options.required && !isEnabled) {
return res.status(404).json({
error: 'Feature not available'
});
}
next();
};
};
// Use middleware
router.get('/new-dashboard',
featureFlagMiddleware('newDashboard', { required: true }),
(req, res) => {
res.render('new-dashboard');
}
);
module.exports = router;
Kubernetes ConfigMaps and Secrets
ConfigMap for Non-Sensitive Config
Copy
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
labels:
app: order-service
data:
# Simple key-value pairs
LOG_LEVEL: "info"
API_TIMEOUT: "5000"
MAX_RETRIES: "3"
# JSON config file
config.json: |
{
"server": {
"port": 3000,
"host": "0.0.0.0"
},
"database": {
"pool": {
"min": 2,
"max": 10
}
},
"features": {
"caching": true,
"compression": true
}
}
# Application properties
application.properties: |
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.maximum-pool-size=10
logging.level.root=INFO
Secrets for Sensitive Data
Copy
# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: order-service-secrets
type: Opaque
data:
# Base64 encoded values
DB_PASSWORD: cGFzc3dvcmQxMjM=
API_KEY: c2VjcmV0LWFwaS1rZXk=
JWT_SECRET: and0LXN1cGVyLXNlY3JldC1rZXk=
---
# For Docker registry credentials
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5leGFtcGxlLmNvbSI6eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MifX19
Using ConfigMaps and Secrets in Deployments
Copy
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: myregistry/order-service:v1
# Environment variables from ConfigMap
envFrom:
- configMapRef:
name: order-service-config
# Individual env vars from secrets
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: order-service-secrets
key: DB_PASSWORD
- name: API_KEY
valueFrom:
secretKeyRef:
name: order-service-secrets
key: API_KEY
# Mount config files
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
- name: secrets-volume
mountPath: /app/secrets
readOnly: true
volumes:
- name: config-volume
configMap:
name: order-service-config
items:
- key: config.json
path: config.json
- name: secrets-volume
secret:
secretName: order-service-secrets
Hot Reload with ConfigMap Updates
Copy
// config/kubernetes-config.js
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const EventEmitter = require('events');
class KubernetesConfig extends EventEmitter {
constructor(configPath = '/app/config') {
super();
this.configPath = configPath;
this.config = {};
}
load() {
const configFile = path.join(this.configPath, 'config.json');
if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, 'utf8');
this.config = JSON.parse(content);
}
return this.config;
}
watch() {
const watcher = chokidar.watch(this.configPath, {
persistent: true,
ignoreInitial: true
});
watcher.on('change', (filePath) => {
console.log(`Config file changed: ${filePath}`);
const oldConfig = { ...this.config };
this.load();
this.emit('change', { oldConfig, newConfig: this.config });
});
return watcher;
}
get(key, defaultValue) {
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return defaultValue;
}
}
return value;
}
}
module.exports = KubernetesConfig;
HashiCorp Vault for Secrets
Vault Integration
Copy
// config/vault-config.js
const vault = require('node-vault');
class VaultConfig {
constructor(options = {}) {
this.client = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR || 'http://localhost:8200',
token: process.env.VAULT_TOKEN
});
this.secretPath = options.secretPath || 'secret/data';
this.serviceName = options.serviceName || process.env.SERVICE_NAME;
this.secrets = {};
this.leases = new Map();
}
// Authenticate with Kubernetes
async authenticateWithKubernetes() {
const jwt = require('fs').readFileSync(
'/var/run/secrets/kubernetes.io/serviceaccount/token',
'utf8'
);
const response = await this.client.kubernetesLogin({
role: this.serviceName,
jwt: jwt
});
this.client.token = response.auth.client_token;
// Set up token renewal
this.scheduleTokenRenewal(response.auth.lease_duration);
}
async loadSecrets() {
const environment = process.env.NODE_ENV || 'development';
// Load service-specific secrets
const servicePath = `${this.secretPath}/${environment}/${this.serviceName}`;
try {
const response = await this.client.read(servicePath);
this.secrets = response.data.data;
console.log(`Loaded secrets for ${this.serviceName}`);
} catch (error) {
if (error.response?.statusCode === 404) {
console.warn(`No secrets found at ${servicePath}`);
this.secrets = {};
} else {
throw error;
}
}
return this.secrets;
}
// Get dynamic database credentials
async getDatabaseCredentials(dbRole = 'readonly') {
const path = `database/creds/${dbRole}`;
try {
const response = await this.client.read(path);
// Schedule credential renewal before expiry
this.scheduleLease(path, response.lease_id, response.lease_duration);
return {
username: response.data.username,
password: response.data.password,
leaseDuration: response.lease_duration
};
} catch (error) {
console.error('Failed to get database credentials:', error);
throw error;
}
}
// Renew lease before expiry
scheduleLease(path, leaseId, leaseDuration) {
// Renew at 75% of lease duration
const renewAt = leaseDuration * 0.75 * 1000;
const timer = setTimeout(async () => {
try {
const response = await this.client.write('sys/leases/renew', {
lease_id: leaseId
});
console.log(`Renewed lease for ${path}`);
this.scheduleLease(path, leaseId, response.lease_duration);
} catch (error) {
console.error(`Failed to renew lease for ${path}:`, error);
// Get new credentials
await this.getDatabaseCredentials(path.split('/').pop());
}
}, renewAt);
this.leases.set(leaseId, timer);
}
scheduleTokenRenewal(leaseDuration) {
const renewAt = leaseDuration * 0.75 * 1000;
setTimeout(async () => {
try {
await this.client.tokenRenewSelf();
console.log('Vault token renewed');
this.scheduleTokenRenewal(leaseDuration);
} catch (error) {
console.error('Failed to renew Vault token:', error);
await this.authenticateWithKubernetes();
}
}, renewAt);
}
get(key, defaultValue) {
return this.secrets[key] || defaultValue;
}
async close() {
for (const timer of this.leases.values()) {
clearTimeout(timer);
}
this.leases.clear();
}
}
module.exports = VaultConfig;
Using Vault in Application
Copy
// app.js
const express = require('express');
const VaultConfig = require('./config/vault-config');
const { Pool } = require('pg');
const app = express();
const vaultConfig = new VaultConfig({ serviceName: 'order-service' });
let dbPool = null;
async function initializeDatabase() {
// Get dynamic credentials from Vault
const credentials = await vaultConfig.getDatabaseCredentials('order-service-rw');
dbPool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: credentials.username,
password: credentials.password,
max: 10
});
// Reconnect when credentials are renewed
vaultConfig.on('credentialsRenewed', async (newCredentials) => {
console.log('Database credentials renewed, reconnecting...');
await dbPool.end();
dbPool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: newCredentials.username,
password: newCredentials.password,
max: 10
});
});
}
async function startServer() {
// Authenticate with Vault (Kubernetes auth)
await vaultConfig.authenticateWithKubernetes();
// Load static secrets
await vaultConfig.loadSecrets();
// Initialize database with dynamic credentials
await initializeDatabase();
const apiKey = vaultConfig.get('API_KEY');
const jwtSecret = vaultConfig.get('JWT_SECRET');
app.listen(3000, () => {
console.log('Server started with Vault integration');
});
}
startServer().catch(console.error);
Environment-Specific Configuration
Multi-Environment Setup
Copy
config/
├── default.json # Base configuration
├── development.json # Dev overrides
├── staging.json # Staging overrides
├── production.json # Prod overrides
└── custom-environment-variables.json # Env var mappings
Copy
// config/loader.js
const fs = require('fs');
const path = require('path');
class ConfigLoader {
constructor(configDir = './config') {
this.configDir = configDir;
this.environment = process.env.NODE_ENV || 'development';
this.config = {};
}
load() {
// Load base config
this.config = this.loadFile('default.json');
// Load environment-specific config
const envConfig = this.loadFile(`${this.environment}.json`);
this.config = this.deepMerge(this.config, envConfig);
// Load local overrides (not committed to git)
const localConfig = this.loadFile('local.json');
this.config = this.deepMerge(this.config, localConfig);
// Apply environment variable overrides
this.applyEnvVars();
return this.config;
}
loadFile(filename) {
const filePath = path.join(this.configDir, filename);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
return {};
}
applyEnvVars() {
const envVarMappings = this.loadFile('custom-environment-variables.json');
this.applyEnvVarsRecursive(this.config, envVarMappings);
}
applyEnvVarsRecursive(config, mappings) {
for (const [key, value] of Object.entries(mappings)) {
if (typeof value === 'object') {
if (!config[key]) config[key] = {};
this.applyEnvVarsRecursive(config[key], value);
} else {
// Value is the env var name
const envValue = process.env[value];
if (envValue !== undefined) {
config[key] = this.parseValue(envValue);
}
}
}
}
parseValue(value) {
// Try to parse as JSON
try {
return JSON.parse(value);
} catch {
return value;
}
}
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] instanceof Object && key in target && !(source[key] instanceof Array)) {
result[key] = this.deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
}
module.exports = new ConfigLoader().load();
Interview Questions
Q1: How do you manage configuration across multiple microservices?
Q1: How do you manage configuration across multiple microservices?
Answer:Use a centralized configuration server (Consul, etcd, Spring Cloud Config):
- Hierarchy: Global → Environment → Service-specific
- Hot reload: Watch for changes without restart
- Secrets: Separate from config (Vault, AWS Secrets Manager)
- Versioning: Track config changes in Git
- Validation: Schema validation on load
- 12-Factor App: Config in environment
- Encryption at rest and in transit
- Audit logging for changes
- Feature flags for gradual rollouts
Q2: How do you implement feature flags?
Q2: How do you implement feature flags?
Answer:Types of feature flags:
- Boolean: Simple on/off
- Percentage: Gradual rollout (10% → 50% → 100%)
- User targeting: Specific users or attributes
- Time-based: Scheduled activation
- Hash user ID for consistent bucketing
- Use configuration store for flag definitions
- SDK in each service to evaluate flags
- Clean up old flags (tech debt)
- Monitor flag usage
- Have kill switches for quick rollback
Q3: How do you handle secrets in microservices?
Q3: How do you handle secrets in microservices?
Answer:Never in code or config files!Solutions:
- HashiCorp Vault: Dynamic secrets, leasing, rotation
- AWS Secrets Manager: AWS-native, automatic rotation
- Kubernetes Secrets: Basic, encode with base64 (not encrypted)
- Dynamic credentials (short-lived)
- Automatic rotation
- Principle of least privilege
- Audit access
- Encrypt at rest
Q4: How do you update configuration without downtime?
Q4: How do you update configuration without downtime?
Answer:Hot Reload Pattern:
- Watch config source for changes
- Validate new config before applying
- Gracefully transition (connection pools, caches)
- Rollback if validation fails
- Update ConfigMap/Secret
- Trigger rolling restart:
kubectl rollout restart - Or use sidecar to watch and signal reload
- Watch config file (chokidar/inotify)
- Poll config server periodically
- Subscribe to config change events
Chapter Summary
Key Takeaways:
- Centralize configuration with tools like Consul or etcd
- Use hierarchical config: global → environment → service
- Implement feature flags for safe progressive rollouts
- Separate secrets from configuration (use Vault)
- Enable hot reload for zero-downtime config updates
- Follow 12-Factor App principles