关于 config-manager
config-manager 是用于设计和实现应用配置管理的 Skill,面向需要处理环境变量、密钥和多环境部署的开发者。它涵盖配置验证、类型安全、功能标志及运行时更新。
主要功能
- 01管理环境变量和配置文件
- 02使用 Zod 验证配置
- 03按环境管理密钥
- 04实现功能标志
- 05支持多环境配置
- 06运行时更新配置
适用场景
- 01为新项目建立配置结构
- 02排查缺失或无效配置
- 03设置开发、测试和生产环境
- 04管理密钥与功能标志
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill config-manager安装后在 Claude Code 中输入/config-manager确认已加载。
详细介绍
配置管理技能
概述
此技能可帮助你为应用设计和实现健壮的配置管理,涵盖环境变量、配置文件、机密管理、验证、类型安全和多环境部署。
配置理念
十二要素应用原则
- 将配置存储在环境中:将配置与代码分离
- 严格分离:不同部署之间存在差异的配置应放在环境变量中
- 代码中不存放机密:绝不例外。
配置层级
Priority (highest to lowest):
1. Command-line arguments
2. Environment variables
3. Environment-specific config files (.env.local)
4. Default config files (.env)
5. Application defaults in code
各项内容的存放位置
- 环境变量:API 密钥、数据库 URL、功能标志
- 配置文件:非敏感默认值、复杂结构
- 机密管理器:生产环境凭据、API 令牌
- 代码:应用逻辑,绝不存放机密
环境变量
.env 文件结构
# .env.example - Commit this file as documentation
# Copy to .env and fill in values
# ===================
# Application
# ===================
NODE_ENV=development
PORT=3000
APP_URL=http://localhost:3000
# ===================
# Database
# ===================
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
DATABASE_POOL_SIZE=10
# ===================
# Authentication
# ===================
# Get from Supabase Dashboard > Settings > API
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# ===================
# External Services
# ===================
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
# ===================
# Email
# ===================
RESEND_API_KEY=re_xxx
[email protected]
# ===================
# Feature Flags
# ===================
ENABLE_ANALYTICS=true
ENABLE_BETA_FEATURES=false
.gitignore 配置
# Environment files
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
# Keep example file
!.env.example
多环境设置
# .env.development
NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/myapp_dev
LOG_LEVEL=debug
# .env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
# .env.production
NODE_ENV=production
DATABASE_URL=${DATABASE_URL} # Set in deployment platform
LOG_LEVEL=info
类型安全的配置
Zod 模式验证
// src/config/env.ts
import { z } from 'zod';
// Define schema
const envSchema = z.object({
// Node environment
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
// Server
PORT: z.coerce.number().default(3000),
APP_URL: z.string().url(),
// Database
DATABASE_URL: z.string().url(),
DATABASE_POOL_SIZE: z.coerce.number().min(1).max(100).default(10),
// Supabase
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
// Stripe (optional in development)
STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_').optional(),
// Feature flags
ENABLE_ANALYTICS: z.coerce.boolean().default(false),
ENABLE_BETA_FEATURES: z.coerce.boolean().default(false),
});
// Type export
export type Env = z.infer<typeof envSchema>;
// Parse and validate
function loadEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:');
console.error(result.error.format());
process.exit(1);
}
return result.data;
}
// Singleton export
export const env = loadEnv();
构建时验证环境变量
// src/config/validate-env.ts
import { z } from 'zod';
// Client-side env (exposed to browser)
const clientEnvSchema = z.object({
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
NEXT_PUBLIC_APP_URL: z.string().url(),
});
// Server-side env (never exposed to browser)
const serverEnvSchema = z.object({
DATABASE_URL: z.string(),
SUPABASE_SERVICE_ROLE_KEY: z.string(),
STRIPE_SECRET_KEY: z.string().optional(),
});
// Combined
const envSchema = clientEnvSchema.merge(serverEnvSchema);
export function validateEnv() {
// Only validate server-side env on server
if (typeof window !== 'undefined') {
return clientEnvSchema.parse({
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
});
}
return envSchema.parse(process.env);
}
T3 Env 模式(Next.js)
// src/env.mjs
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
/**
* Server-side environment variables
*/
server: {
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
SUPABASE_SERVICE_ROLE_KEY: z.string(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
},
/**
* Client-side environment variables (exposed to browser)
* Prefix with NEXT_PUBLIC_
*/
client: {
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(),
NEXT_PUBLIC_APP_URL: z.string().url(),
},
/**
* Runtime values
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
/**
* Skip validation in certain environments
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});
配置文件
JSON 配置
// config/default.json
{
"app": {
"name": "My Application",
"version": "1.0.0"
},
"server": {
"port": 3000,
"host": "localhost"
},
"cache": {
"ttl": 3600,
"maxSize": 1000
},
"features": {
"darkMode": true,
"betaFeatures": false
}
}
// config/production.json (overrides default)
{
"server": {
"host": "0.0.0.0"
},
"cache": {
"ttl": 86400
}
}
配置加载器
// src/config/loader.ts
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { z } from 'zod';
const configSchema = z.object({
app: z.object({
name: z.string(),
version: z.string(),
}),
server: z.object({
port: z.number(),
host: z.string(),
}),
cache: z.object({
ttl: z.number(),
maxSize: z.number(),
}),
features: z.object({
darkMode: z.boolean(),
betaFeatures: z.boolean(),
}),
});
type Config = z.infer<typeof configSchema>;
function deepMerge(target: any, source: any): any {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
export function loadConfig(): Config {
const env = process.env.NODE_ENV || 'development';
const configDir = join(process.cwd(), 'config');
// Load default config
const defaultPath = join(configDir, 'default.json');
let config = JSON.parse(readFileSync(defaultPath, 'utf-8'));
// Merge environment-specific config
const envPath = join(configDir, `${env}.json`);
if (existsSync(envPath)) {
const envConfig = JSON.parse(readFileSync(envPath, 'utf-8'));
config = deepMerge(config, envConfig);
}
// Merge local overrides (not committed)
const localPath = join(configDir, 'local.json');
if (existsSync(localPath)) {
const localConfig = JSON.parse(readFileSync(localPath, 'utf-8'));
config = deepMerge(config, localConfig);
}
// Validate
return configSchema.parse(config);
}
export const config = loadConfig();
机密管理
本地开发机密
# Use a secrets manager locally too
# Option 1: 1Password CLI
op read "op://Development/MyApp/API_KEY"
# Option 2: Doppler
doppler secrets download --no-file --format env > .env
# Option 3: AWS SSM (for local AWS dev)
aws ssm get-parameter --name "/myapp/dev/api-key" --with-decryption --query "Parameter.Value"
使用 Vercel 管理生产环境机密
# Add secrets via CLI
vercel env add STRIPE_SECRET_KEY production
vercel env add DATABASE_URL production
# Pull secrets to local .env
vercel env pull .env.local
Docker 中的机密
# docker-compose.yml
version: '3.8'
services:
app:
build: .
environment:
- NODE_ENV=production
env_file:
- .env
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true # From Docker Swarm/secrets manager
AWS Secrets Manager 集成
// src/config/secrets.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
interface AppSecrets {
database_url: string;
stripe_secret_key: string;
jwt_secret: string;
}
let cachedSecrets: AppSecrets | null = null;
export async function getSecrets(): Promise<AppSecrets> {
if (cachedSecrets) {
return cachedSecrets;
}
const secretId = process.env.AWS_SECRET_ID || 'myapp/production/secrets';
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await client.send(command);
if (!response.SecretString) {
throw new Error('Secret not found');
}
cachedSecrets = JSON.parse(response.SecretString);
return cachedSecrets!;
}
// Usage
async function connectDatabase() {
const secrets = await getSecrets();
return createConnection(secrets.database_url);
}
功能标志
简单的功能标志系统
// src/config/features.ts
import { env } from './env';
export const features = {
analytics: env.ENABLE_ANALYTICS,
betaFeatures: env.ENABLE_BETA_FEATURES,
darkMode: true, // Always enabled
// Computed flags
get isProduction() {
return env.NODE_ENV === 'production';
},
get enableDebugLogs() {
return env.NODE_ENV === 'development' || env.DEBUG === 'true';
},
} as const;
// Type-safe feature checking
export function isEnabled(feature: keyof typeof features): boolean {
return Boolean(features[feature]);
}
// Usage
if (isEnabled('analytics')) {
initAnalytics();
}
与环境关联的功能标志
// src/config/feature-flags.ts
type Environment = 'development' | 'staging' | 'production';
interface FeatureConfig {
enabled: boolean | Environment[];
description: string;
}
const featureFlags: Record<string, FeatureConfig> = {
newCheckout: {
enabled: ['development', 'staging'],
description: 'New checkout flow',
},
aiAssistant: {
enabled: true,
description: 'AI assistant feature',
},
experimentalApi: {
enabled: ['development'],
description: 'Experimental API endpoints',
},
};
export function isFeatureEnabled(
feature: keyof typeof featureFlags
): boolean {
const config = featureFlags[feature];
const currentEnv = process.env.NODE_ENV as Environment;
if (typeof config.enabled === 'boolean') {
return config.enabled;
}
return config.enabled.includes(currentEnv);
}
配置模式
单例配置
// src/config/index.ts
import { env } from './env';
import { loadConfig } from './loader';
import { features } from './features';
class AppConfig {
private static instance: AppConfig;
public readonly env = env;
public readonly features = features;
public readonly settings = loadConfig();
private constructor() {
// Freeze to prevent modifications
Object.freeze(this);
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
// Helper methods
get isDevelopment(): boolean {
return this.env.NODE_ENV === 'development';
}
get isProduction(): boolean {
return this.env.NODE_ENV === 'production';
}
get databaseUrl(): string {
return this.env.DATABASE_URL;
}
}
export const config = AppConfig.getInstance();
运行时配置更新
// src/config/runtime.ts
import { EventEmitter } from 'events';
class RuntimeConfig extends EventEmitter {
private values: Map<string, any> = new Map();
get<T>(key: string, defaultValue?: T): T | undefined {
return this.values.get(key) ?? defaultValue;
}
set<T>(key: string, value: T): void {
const oldValue = this.values.get(key);
this.values.set(key, value);
this.emit('change', { key, oldValue, newValue: value });
}
// Subscribe to changes
onChange(callback: (change: { key: string; oldValue: any; newValue: any }) => void) {
this.on('change', callback);
return () => this.off('change', callback);
}
}
export const runtimeConfig = new RuntimeConfig();
// Usage: Update config at runtime
runtimeConfig.set('maintenanceMode', true);
// Usage: React to changes
runtimeConfig.onChange(({ key, newValue }) => {
if (key === 'maintenanceMode' && newValue) {
showMaintenanceBanner();
}
});
验证清单
环境设置
-
.env.example中记录了所有变量 -
.gitignore排除所有.env文件,但保留示例文件 - 应用启动时执行验证
- 为缺失或无效的配置提供有帮助的错误消息
- 为所有配置值定义类型
安全
- 版本控制中不包含机密
- 使用适当的机密管理器管理机密
- 定期轮换生产环境机密
- 尽量减少敏感值在日志中的暴露
- 仅对真正公开的值使用
NEXT_PUBLIC_前缀
开发者体验
- 本地设置简单(复制
.env.example) - 清晰记录每个变量
- 为开发环境设置默认值
- 配置验证提供可执行的错误信息
- IDE 可自动补全配置值
多环境
- 为开发、预发布和生产环境分别配置
- 环境专属覆盖项可正常工作
- 使用功能标志逐步发布
- 可轻松切换环境
何时使用此技能
在以下情况下调用此技能:
- 为新项目设置配置
- 添加新的环境变量
- 实现机密管理
- 创建功能标志系统
- 调试配置问题
- 设置多环境部署
- 在不同提供商之间迁移配置
- 实现运行时配置变更
评论
支持 Markdown