Dotenvx + TOML 集成最佳实践

Dotenvx + TOML 集成最佳实践

概述

TOML (Tom’s Obvious, Minimal Language) 是一种简洁、易读的配置文件格式。结合 dotenvx 使用可以实现:

  • 配置分离:TOML 存储非敏感配置,dotenvx 管理敏感信息
  • 类型安全:TOML 支持丰富的数据类型
  • 层次结构:清晰的配置组织结构
  • 版本控制友好:TOML 文件可以安全提交到代码仓库

核心设计原则

1. 配置分离原则

📁 项目结构
├── config.toml          # 非敏感应用配置
├── .env                 # 敏感信息(本地开发)
├── .env.production      # 生产环境敏感信息(加密)
├── .env.example         # 环境变量模板
└── .gitignore           # 排除敏感文件

2. 配置优先级

环境变量 > .env 文件 > TOML 配置 > 默认值

3. 命名约定

# TOML 中使用 snake_case
[database]
max_connections = 100
connection_timeout = 30

# 环境变量使用 UPPER_SNAKE_CASE
DB_HOST=localhost
DB_PASSWORD=secret123

实际应用场景

场景1:Web 应用配置管理

config.toml

[app]
name = "MyWebApp"
version = "2.1.0"
debug = false
timezone = "UTC"

[server]
host = "0.0.0.0"
port = 3000
workers = 4
keep_alive_timeout = 65
request_timeout = 30
max_request_size = "10MB"

[database]
driver = "postgresql"
pool_size = 20
max_overflow = 30
pool_timeout = 30
pool_recycle = 3600
echo_sql = false

[cache]
backend = "redis"
default_timeout = 300
key_prefix = "myapp:"
serializer = "json"

[logging]
level = "info"
format = "json"
rotation = "daily"
max_files = 30

[security]
cors_origins = ["http://localhost:3000", "https://myapp.com"]
rate_limit_per_minute = 100
session_timeout = 1800

[features]
user_registration = true
email_verification = true
two_factor_auth = false
analytics = true

.env.production (加密)

DOTENV_PUBLIC_KEY_PRODUCTION="026d4945b6513baec60f68b207f203ba534fb54d2b0c9952557d240815e42a7d83"

# 数据库连接
DB_HOST="encrypted:BMO83g2fEtr66gcFvUs2+/ZuccCQuBbZwSW3JfCLvoUiACmusxCbTfG2dvc2LxenPhUtgWapO8f9BCcBVAcTnMcrd3kndvk+acWytRjIWRUvsSezdD340/OT5EQgbqJtwXfuRz0i2t8PVA=="
DB_PORT="encrypted:BGcRf5bK/mChGEqT1MZ8hUbMm3hhtuW9NVGkHtl7KRwqbSKnVcGIDs9T61u77DlyNlYcF1BlLCw9HPmbRQ0nFvLOCZc6r42iRE4OyJw9mu61OjlWQfEl5Z1NrjZw5g0d1tp8New="
DB_NAME="encrypted:BL0icNnZh6InVmymJBCX6MuL6cwgVc4v1ua1g1XONlV7nkzzHHHpnZN3khx7+ld15bd88EtV4DfqUV2eJ/HJwu0/5F1MH+PAisYSRxBQo8I9AHly2sRsonBm3Bji+DslcC4D7b7wLTBlfCw="
DB_USER="encrypted:BBrXv55qxgA19sEqqNnZzS/C0WguVk6ROQmfxnGhBhafLoc0XwpKprk/J3hJCVq7s45WyBSXGUz9U9rHxCBeVkl27WFzzgZkDewX0gBLt+Cc37K0EVU2hZ1GPbax5mzpI5Jwwi65be6+"
DB_PASSWORD="encrypted:BC8aRBQ/Q2YMPjJayggqVN8skqTtxtXFgYA0e8/Ud/Jcez2Daukr6edBmEWQdz/Lu91casaW6CkkCvLSQkPvNpmgYqFB4BKHTUDowX/KEDvVI6CU5Vt478VF5dqHbvPIoKKtBe+4FNXlk5O96A=="

# 缓存连接
REDIS_HOST="encrypted:BMVEIPBGe9xkELFb48KQJPxxnTkUGhsonAU4ug5ca9E5eD/MZimkoQrf/3cb9nhazwfTbScLgeGGr/Jhj4DV7Xpz45XEEFWrPXy1Yi93zWLaJ4XYBHwCke3b4XCbh7jV4uL3WWFjI757yTIS6ilD"
REDIS_PASSWORD="encrypted:BIgpV7btyiGYyySYnG3+NJVGUzNzB4zWjIZbM/VgtnPuiuSsK/KBkirtqkDBI8U/04BRKtupOTNSJTVu6GO39XPSpPvlxA4fNRyeK85W+rFGARp4mrgqfEz/O/eZvqJSqS5kNraAhbkKpXq81rEOBg=="

# 安全密钥
JWT_SECRET="encrypted:BCrnJ2sAZH2qwRlPvUqqWyEsd+cVeMQiOV5H/xZ7vjFfcMXHMunmAv/7+jUI356fkVtHfrXu+vBJLjXJiirgB2gky5vvy7h5jevgMS6BgPL5KwjC0tYPlYbe4Bfrf1funYqqrFYaPjsEO+77vCtVaBPz"
SESSION_SECRET="encrypted:BCrnJ2sAZH2qwRlPvUqqWyEsd+cVeMQiOV5H/xZ7vjFfcMXHMunmAv/7+jUI356fkVtHfrXu+vBJLjXJiirgB2gky5vvy7h5jevgMS6BgPL5KwjC0tYPlYbe4Bfrf1funYqqrFYaPjsEO+77vCtVaBPz"

# 第三方服务
STRIPE_SECRET_KEY="encrypted:BOD5Fg+qI9dqhkh+gjCLrTFyhxEAhNDtLgwjkMZOr9l9CsvvhprwCrgsZbIRIFa1Vf6ATnWZ3/bacYnlBXlZ1Hc6YMZHog+ZuVW4AjwxCkB8I0AkcOeOsYzQx2fdtI4kFii01UIhN53jfmUjzLSPYw=="
SENDGRID_API_KEY="encrypted:BCrnJ2sAZH2qwRlPvUqqWyEsd+cVeMQiOV5H/xZ7vjFfcMXHMunmAv/7+jUI356fkVtHfrXu+vBJLjXJiirgB2gky5vvy7h5jevgMS6BgPL5KwjC0tYPlYbe4Bfrf1funYqqrFYaPjsEO+77vCtVaBPz"

配置加载器 (config-loader.js)

const fs = require('fs')
const path = require('path')
const toml = require('toml')
require('@dotenvx/dotenvx').config()

class ConfigLoader {
  constructor() {
    this.config = null
    this.load()
  }

  load() {
    try {
      // 读取 TOML 基础配置
      const configPath = path.join(process.cwd(), 'config.toml')
      const configFile = fs.readFileSync(configPath, 'utf8')
      const baseConfig = toml.parse(configFile)

      // 合并环境变量
      this.config = this.mergeWithEnv(baseConfig)
      
      // 验证配置
      this.validate()
      
      console.log('✅ Configuration loaded successfully')
    } catch (error) {
      console.error('❌ Failed to load configuration:', error.message)
      process.exit(1)
    }
  }

  mergeWithEnv(baseConfig) {
    return {
      ...baseConfig,
      
      // 数据库配置合并
      database: {
        ...baseConfig.database,
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT) || 5432,
        name: process.env.DB_NAME || 'myapp',
        user: process.env.DB_USER || 'postgres',
        password: process.env.DB_PASSWORD || '',
        url: this.buildDatabaseUrl()
      },

      // 缓存配置合并
      cache: {
        ...baseConfig.cache,
        host: process.env.REDIS_HOST || 'localhost',
        port: parseInt(process.env.REDIS_PORT) || 6379,
        password: process.env.REDIS_PASSWORD || '',
        url: this.buildRedisUrl()
      },

      // 安全配置合并
      security: {
        ...baseConfig.security,
        jwtSecret: process.env.JWT_SECRET || '',
        sessionSecret: process.env.SESSION_SECRET || '',
        encryptionKey: process.env.ENCRYPTION_KEY || ''
      },

      // 第三方服务配置
      services: {
        stripe: {
          secretKey: process.env.STRIPE_SECRET_KEY || '',
          publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || ''
        },
        sendgrid: {
          apiKey: process.env.SENDGRID_API_KEY || ''
        },
        aws: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
          region: process.env.AWS_REGION || 'us-east-1'
        }
      }
    }
  }

  buildDatabaseUrl() {
    const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env
    return `postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`
  }

  buildRedisUrl() {
    const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD } = process.env
    const auth = REDIS_PASSWORD ? `:${REDIS_PASSWORD}@` : ''
    return `redis://${auth}${REDIS_HOST}:${REDIS_PORT}`
  }

  validate() {
    const required = [
      'DB_HOST', 'DB_PASSWORD', 'JWT_SECRET', 'SESSION_SECRET'
    ]

    const missing = required.filter(key => !process.env[key])
    
    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`)
    }

    // 验证数据库连接配置
    if (!this.config.database.url.includes('postgresql://')) {
      throw new Error('Invalid database URL format')
    }

    // 验证 JWT 密钥长度
    if (process.env.JWT_SECRET.length < 32) {
      throw new Error('JWT_SECRET must be at least 32 characters long')
    }
  }

  get() {
    return this.config
  }

  // 获取特定配置节
  getDatabase() {
    return this.config.database
  }

  getCache() {
    return this.config.cache
  }

  getSecurity() {
    return this.config.security
  }

  getServices() {
    return this.config.services
  }
}

// 单例模式
const configLoader = new ConfigLoader()
module.exports = configLoader.get()

场景2:微服务架构配置

services.toml

[services.user]
name = "user-service"
port = 3001
health_check = "/health"
timeout = 30

[services.order]
name = "order-service"
port = 3002
health_check = "/health"
timeout = 45

[services.payment]
name = "payment-service"
port = 3003
health_check = "/health"
timeout = 60

[discovery]
consul_host = "consul"
consul_port = 8500
health_check_interval = "10s"
deregister_critical_after = "30s"

[load_balancer]
strategy = "round_robin"
health_check_enabled = true
retry_attempts = 3

.env.services

# 服务发现
CONSUL_TOKEN=your_consul_token
SERVICE_REGISTRY_AUTH=your_registry_auth

# 服务间通信密钥
SERVICE_MESH_KEY=your_service_mesh_key
INTER_SERVICE_JWT=your_inter_service_jwt

# 监控和追踪
JAEGER_ENDPOINT=http://jaeger:14268/api/traces
PROMETHEUS_PUSHGATEWAY=http://prometheus:9091

工具和库推荐

Node.js 生态

npm install toml @dotenvx/dotenvx joi convict

Python 生态

pip install toml python-dotenv pydantic

Go 生态

go get github.com/BurntSushi/toml
go get github.com/joho/godotenv

Rust 生态

[dependencies]
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
dotenv = "0.15"

部署和运维

Docker 集成

FROM node:18-alpine

WORKDIR /app

# 复制配置文件
COPY config.toml ./
COPY .env.production ./

# 安装依赖
COPY package*.json ./
RUN npm ci --only=production
RUN npm install -g @dotenvx/dotenvx

# 复制应用代码
COPY . .

# 使用 dotenvx 运行
CMD ["dotenvx", "run", "-f", ".env.production", "--", "node", "app.js"]

Kubernetes ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  config.toml: |
    [app]
    name = "MyApp"
    version = "1.0.0"
    
    [server]
    host = "0.0.0.0"
    port = 3000
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  .env.production: <base64-encoded-encrypted-env-file>
  DOTENV_PRIVATE_KEY_PRODUCTION: <base64-encoded-private-key>

监控和调试

配置验证脚本

// validate-config.js
const config = require('./config-loader')

console.log('🔍 Validating configuration...')
console.log('📊 Database:', config.database.host)
console.log('🔄 Cache:', config.cache.host)
console.log('🔐 Security keys:', Object.keys(config.security))
console.log('🌐 Services:', Object.keys(config.services))
console.log('✅ Configuration is valid!')

运行验证

# 验证开发环境配置
dotenvx run -f .env.development -- node validate-config.js

# 验证生产环境配置
dotenvx run -f .env.production -- node validate-config.js

最佳实践总结

✅ 推荐做法

  1. 配置分离:TOML 存储结构化配置,dotenvx 管理敏感信息
  2. 类型验证:使用 Joi、Pydantic 等库验证配置
  3. 环境特定:为不同环境创建对应的配置文件
  4. 文档化:为每个配置项添加注释说明
  5. 版本控制:TOML 文件提交到仓库,敏感信息加密存储

❌ 避免做法

  1. 混合存储:不要在 TOML 中存储敏感信息
  2. 硬编码:避免在代码中硬编码配置值
  3. 过度复杂:保持配置结构简单清晰
  4. 缺少验证:不验证配置的完整性和正确性
  5. 忽略安全:不要忽视配置文件的安全性

通过合理使用 TOML + dotenvx 的组合,可以构建出既安全又易维护的配置管理系统。