configuration-management
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConfiguration Management
配置管理
Overview
概述
Comprehensive guide to managing application configuration across environments, including environment variables, configuration files, secrets, feature flags, and following 12-factor app methodology.
本指南全面介绍了跨环境管理应用程序配置的方法,包括环境变量、配置文件、密钥、Feature Flags,以及遵循12-factor app方法论。
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
- 为不同环境配置应用程序
- 管理密钥和凭证
- 实现Feature Flags
- 创建配置层级
- 遵循12-factor app原则
- 将配置迁移到云服务
- 实现动态配置
- 管理多租户配置
Instructions
操作指南
1. Environment Variables
1. Environment Variables
Basic Setup (.env files)
基础设置(.env文件)
bash
undefinedbash
undefined.env.development
.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
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
.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
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
.env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
undefinedNODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
undefinedLoading Environment Variables
加载环境变量
typescript
// 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;typescript
// 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
Python配置
python
undefinedpython
undefinedconfig/settings.py
config/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
import os
from pathlib import Path
from dotenv import load_dotenv
Load .env file
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:'
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
Configuration dictionary
config_by_name = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'test': TestConfig
}
config_by_name = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'test': TestConfig
}
Get active config
Get active config
config = config_by_nameConfig.ENV
undefinedconfig = config_by_nameConfig.ENV
undefined2. Configuration Hierarchies
2. 配置层级
typescript
// 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'];typescript
// 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
YAML配置文件
yaml
undefinedyaml
undefinedconfig/default.yml
config/default.yml
app:
name: MyApp
version: 1.0.0
server:
timeout: 30000
bodyLimit: 100kb
database:
poolSize: 10
idleTimeout: 30000
app:
name: MyApp
version: 1.0.0
server:
timeout: 30000
bodyLimit: 100kb
database:
poolSize: 10
idleTimeout: 30000
config/development.yml
config/development.yml
server:
port: 3000
database:
url: postgresql://localhost:5432/myapp_dev
logging: true
logging:
level: debug
prettyPrint: true
server:
port: 3000
database:
url: postgresql://localhost:5432/myapp_dev
logging: true
logging:
level: debug
prettyPrint: true
config/production.yml
config/production.yml
server:
port: 8080
trustProxy: true
database:
url: ${DATABASE_URL}
ssl: true
logging: false
logging:
level: info
prettyPrint: false
```typescript
// 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');server:
port: 8080
trustProxy: true
database:
url: ${DATABASE_URL}
ssl: true
logging: false
logging:
level: info
prettyPrint: false
```typescript
// 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
3. 密钥管理
AWS Secrets Manager
AWS Secrets Manager
typescript
// 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
});
}typescript
// 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
HashiCorp Vault
typescript
// 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
};
}
}typescript
// 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
环境专属密钥
typescript
// 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();
}typescript
// 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
4. Feature Flags
Simple Feature Flag Implementation
简单Feature Flags实现
typescript
// 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());
});typescript
// 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
LaunchDarkly集成
typescript
// 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());
});typescript
// 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
5. 12-Factor App Configuration
typescript
// 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);
});
}typescript
// config/twelve-factor.ts
/**
* 12-Factor App配置原则
*
* III. 配置 - 将配置存储在环境中
* - 严格区分配置与代码
* - 配置随部署环境变化,代码保持不变
* - 存储在环境变量中
*/
// ✅ 良好实践:从环境中获取配置
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!
}
};
// ❌ 不良实践:硬编码配置
const badConfig = {
database: {
host: 'prod-db.example.com', // 硬编码!
password: 'secretpassword' // 密钥写入代码!
}
};
/**
* 后端服务 - 将后端服务视为附加资源
* - 数据库、缓存、消息队列等通过URL访问
* - 无需修改代码即可替换服务
*/
// ✅ 良好实践:通过URL访问后端服务
const db = createConnection(process.env.DATABASE_URL);
const cache = createClient(process.env.REDIS_URL);
// 通过修改环境变量即可替换服务
// DATABASE_URL=postgresql://localhost/dev (本地开发)
// DATABASE_URL=postgresql://prod-db/app (生产环境)
/**
* 可处置性 - 快速启动与优雅关闭
*/
function startServer() {
const server = app.listen(config.port, () => {
console.log(`服务器已启动,端口:${config.port}`);
});
// 优雅关闭
process.on('SIGTERM', async () => {
console.log('收到SIGTERM信号,开始优雅关闭');
server.close(() => {
console.log('HTTP服务器已关闭');
});
await db.close();
await cache.quit();
process.exit(0);
});
}6. Configuration Validation
6. 配置验证
typescript
// 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();typescript
// 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, // 允许存在其他环境变量
stripUnknown: true // 移除未知变量
});
if (error) {
throw new Error(`配置验证错误:${error.message}`);
}
return value;
}
// 使用示例
const validatedConfig = validateConfig();7. Dynamic Configuration (Remote Config)
7. 动态配置(远程配置)
typescript
// 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
});typescript
// 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(`配置已变更:${key} = ${value}`);
this.config.set(key, value);
}
}
} catch (error) {
console.error('获取远程配置失败:', error);
}
}
private startPolling() {
// 每60秒轮询一次
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);
}
}
}
// 使用示例
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);
// 使用动态配置值
});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
- 将配置存储在环境变量中
- 为不同环境使用独立的配置文件
- 在启动时验证配置
- 使用密钥管理器存储敏感数据
- 切勿将密钥提交到版本控制系统
- 提供合理的默认值
- 记录所有配置选项
- 使用类型安全的配置对象
- 实现配置层级(基础配置 + 环境覆盖)
- 使用Feature Flags实现渐进式发布
- 遵循12-factor app原则
- 为缺失的配置实现优雅降级
- 缓存密钥以减少API调用
❌ 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
- 在源代码中硬编码配置
- 提交包含真实密钥的.env文件
- 在不同服务中使用不同的配置格式
- 明文存储密钥
- 通过API暴露配置
- 在开发环境中使用生产凭证
- 忽略配置验证错误
- 在代码各处直接访问process.env
- 将配置存储在数据库中(循环依赖)
- 将配置与业务逻辑混合
Common Patterns
常见模式
Pattern 1: Config Service
模式1:配置服务
typescript
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];
}
}typescript
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
模式2:配置构建器
typescript
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;
}
}typescript
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
- dotenv: 从.env文件加载环境变量
- convict: 带验证的配置管理工具
- config: Node.js的层级配置工具
- AWS Secrets Manager: 云原生密钥存储服务
- HashiCorp Vault: 密钥与加密管理工具
- LaunchDarkly: Feature Flags管理平台
- ConfigCat: Feature Flags与配置服务
- Consul: 服务配置与发现工具