Skip to main content

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

┌─────────────────────────────────────────────────────────────────────────────┐
│                      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:
// ❌ 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

// 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

// 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

// 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

// 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

// 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

// 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

# 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

# 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

# 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

// 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

// 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

// 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

config/
├── default.json          # Base configuration
├── development.json      # Dev overrides
├── staging.json         # Staging overrides
├── production.json      # Prod overrides
└── custom-environment-variables.json  # Env var mappings
// 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

Answer:Use a centralized configuration server (Consul, etcd, Spring Cloud Config):
  1. Hierarchy: Global → Environment → Service-specific
  2. Hot reload: Watch for changes without restart
  3. Secrets: Separate from config (Vault, AWS Secrets Manager)
  4. Versioning: Track config changes in Git
  5. Validation: Schema validation on load
Best Practices:
  • 12-Factor App: Config in environment
  • Encryption at rest and in transit
  • Audit logging for changes
  • Feature flags for gradual rollouts
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
Implementation:
  • Hash user ID for consistent bucketing
  • Use configuration store for flag definitions
  • SDK in each service to evaluate flags
Best Practices:
  • Clean up old flags (tech debt)
  • Monitor flag usage
  • Have kill switches for quick rollback
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)
Best Practices:
  • Dynamic credentials (short-lived)
  • Automatic rotation
  • Principle of least privilege
  • Audit access
  • Encrypt at rest
Answer:Hot Reload Pattern:
  1. Watch config source for changes
  2. Validate new config before applying
  3. Gracefully transition (connection pools, caches)
  4. Rollback if validation fails
Kubernetes approach:
  • Update ConfigMap/Secret
  • Trigger rolling restart: kubectl rollout restart
  • Or use sidecar to watch and signal reload
Application approach:
  • 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
Next Chapter: CI/CD for Microservices - Pipelines and deployment strategies.