配置管理
概览
全面的指南,用于管理跨环境的应用程序配置,包括环境变量、配置文件、秘密、特性标志以及遵循12因素应用程序方法论。
何时使用
- 为不同环境设置配置
- 管理秘密和凭证
- 实施特性标志
- 创建配置层级
- 遵循12因素应用程序原则
- 将配置迁移到云服务
- 实施动态配置
- 管理多租户配置
指令
1. 环境变量
基本设置(.env文件)
# .env.development
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
LOG_LEVEL=debug
API_KEY=dev-api-key-12345
# .env.production
NODE_ENV=production
PORT=8080
DATABASE_URL=${DATABASE_URL} # 来自环境
REDIS_URL=${REDIS_URL}
LOG_LEVEL=info
API_KEY=${API_KEY} # 来自秘密管理器
# .env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
加载环境变量
// config/env.ts
import dotenv from 'dotenv';
import path from 'path';
// 加载特定于环境的.env文件
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
// 验证必需的变量
const required = ['DATABASE_URL', 'PORT', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`缺少必需的环境变量:${missing.join(', ')}`);
}
// 导出类型化的配置
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配置
# config/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# 加载.env文件
env_file = f'.env.{os.getenv("ENVIRONMENT", "development")}'
load_dotenv(Path(__file__).parent.parent / env_file)
class Config:
"""基础配置"""
ENV = os.getenv('ENVIRONMENT', 'development')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
SECRET_KEY = os.getenv('SECRET_KEY')
# 数据库
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):
"""开发配置"""
DEBUG = True
LOG_LEVEL = 'DEBUG'
class ProductionConfig(Config):
"""生产配置"""
DEBUG = False
LOG_LEVEL = 'INFO'
class TestConfig(Config):
"""测试配置"""
TESTING = True
DATABASE_URL = 'sqlite:///:memory:'
# 配置字典
config_by_name = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'test': TestConfig
}
# 获取活动配置
config = config_by_name[Config.ENV]()
2. 配置层级
// config/config.ts
import deepmerge from 'deepmerge';
// 基础配置(所有环境共享)
const baseConfig = {
app: {
name: 'MyApp',
version: '1.0.0'
},
server: {
timeout: 30000,
bodyLimit: '100kb'
},
database: {
poolSize: 10,
idleTimeout: 30000
},
logging: {
format: 'json',
destination: 'stdout'
}
};
// 环境特定的覆盖
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
}
};
// 合并配置
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配置文件
# config/default.yml
app:
name: MyApp
version: 1.0.0
server:
timeout: 30000
bodyLimit: 100kb
database:
poolSize: 10
idleTimeout: 30000
# config/development.yml
server:
port: 3000
database:
url: postgresql://localhost:5432/myapp_dev
logging: true
logging:
level: debug
prettyPrint: true
# config/production.yml
server:
port: 8080
trustProxy: true
database:
url: ${DATABASE_URL}
ssl: true
logging: false
logging:
level: info
prettyPrint: false
// 加载YAML配置
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. 秘密管理
AWS Secrets Manager
// secrets/aws-secrets-manager.ts
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
export class SecretManager {
private client: SecretsManagerClient;
private cache = new Map<string, { value: any; expiry: number }>();
private cacheTtl = 300000; // 5分钟
constructor() {
this.client = new SecretsManagerClient({ region: process.env.AWS_REGION });
}
async getSecret(secretName: string): Promise<any> {
// 检查缓存
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 || '{}');
// 缓存秘密
this.cache.set(secretName, {
value: secret,
expiry: Date.now() + this.cacheTtl
});
return secret;
} catch (error) {
throw new Error(`无法检索秘密${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];
}
}
// 使用
const secretManager = new SecretManager();
async function connectDatabase() {
const credentials = await secretManager.getDatabaseCredentials();
return createConnection({
host: credentials.host,
port: credentials.port,
username: credentials.username,
password: credentials.password,
database: credentials.database
});
}
HashiCorp Vault
// secrets/vault.ts
import vault from 'node-vault';
export class VaultClient {
private client: any;
constructor() {
this.client = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR || 'http://localhost:8200',
token: process.env.VAULT_TOKEN
});
}
async getSecret(path: string): Promise<any> {
try {
const result = await this.client.read(path);
return result.data.data;
} catch (error) {
throw new Error(`无法从${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');
}
// 动态数据库凭证(自动旋转)
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
};
}
}
环境特定的秘密
// secrets/secret-provider.ts
export interface SecretProvider {
getSecret(key: string): Promise<string>;
}
// 开发:使用.env文件
export class EnvFileSecretProvider implements SecretProvider {
async getSecret(key: string): Promise<string> {
const value = process.env[key];
if (!value) {
throw new Error(`秘密${key}在环境中未找到`);
}
return value;
}
}
// 生产:使用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];
}
}
// 工厂
export function createSecretProvider(): SecretProvider {
if (process.env.NODE_ENV === 'production') {
return new AWSSecretProvider();
}
return new EnvFileSecretProvider();
}
4. 特性标志
简单的特性标志实现
// 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;
// 检查是否全局禁用
if (!flag.enabled) return false;
// 检查环境限制
if (flag.allowedEnvironments && context?.environment) {
if (!flag.allowedEnvironments.includes(context.environment)) {
return false;
}
}
// 检查用户白名单
if (flag.allowedUsers && context?.userId) {
if (flag.allowedUsers.includes(context.userId)) {
return true;
}
}
// 检查推出百分比
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);
}
}
// 配置
const featureFlags = {
'new-dashboard': {
enabled: true,
rolloutPercentage: 50 // 50%的用户
},
'experimental-feature': {
enabled: true,
allowedUsers: ['user-123', 'user-456'],
allowedEnvironments: ['development', 'staging']
},
'beta-api': {
enabled: true,
rolloutPercentage: 10
}
};
const flagManager = new FeatureFlagManager(featureFlags);
// 使用
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集成
// 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();
}
}
// 使用
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配置
// 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. 配置验证
// 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. 动态配置(远程配置)
// 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);
// 使用动态配置值
});
最佳实践
✅ 做
- 在环境变量中存储配置
- 为每个环境使用不同的配置文件
- 在启动时验证配置
- 使用秘密管理器处理敏感数据
- 永远不要将秘密提交到版本控制
- 提供合理的默认值
- 记录所有配置选项
- 使用类型安全的配置对象
- 实施配置层级(基础+覆盖)
- 使用特性标志进行逐步推出
- 遵循12因素应用程序原则
- 实施对缺失配置的优雅降级
- 缓存秘密以减少API调用
❌ 不做
- 在源代码中硬编码配置
- 提交带有真实秘密的.env文件
- 在服务之间使用不同的配置格式
- 以纯文本形式存储秘密
- 通过API暴露配置
- 在开发中使用生产凭证
- 忽略配置验证错误
- 到处直接访问process.env
- 在数据库中存储配置(循环依赖)
- 将配置与业务逻辑混合
常见模式
模式1:配置服务
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];
}
}
模式2:配置构建器
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;
}
}
工具和资源
- dotenv:从.env文件中加载环境变量
- convict:带验证的配置管理
- config:Node.js的分层配置
- AWS Secrets Manager:基于云的秘密存储
- HashiCorp Vault:秘密和加密管理
- LaunchDarkly:特性标志管理
- ConfigCat:特性标志和配置服务
- Consul:服务配置和发现