config-manager について
config-manager は、環境変数、シークレット、複数環境のデプロイを扱う開発者向けの設定管理スキルです。設定の検証や型安全性、フィーチャーフラグ、実行時更新を扱います。
主な機能
- 01環境変数と設定ファイルの管理
- 02Zod による設定検証
- 03環境別のシークレット管理
- 04フィーチャーフラグの実装
- 05複数環境の設定対応
- 06実行時の設定更新
活用シーン
- 01新規プロジェクトの設定構成
- 02不足または無効な設定の調査
- 03開発・テスト・本番環境の設定
- 04シークレットとフィーチャーフラグの管理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill config-managerインストール後、Claude Code で/config-managerを入力して読み込まれたか確認します。
詳細
設定管理スキル
概要
このスキルは、アプリケーション向けの堅牢な設定管理の設計と実装に役立ちます。環境変数、設定ファイル、シークレット管理、検証、型安全性、複数環境へのデプロイを扱います。
設定の考え方
Twelve-Factor App の原則
- 設定は環境に保存する:設定をコードから分離します
- 厳密に分離する:デプロイ間で異なる設定は環境変数にします
- コードにシークレットを含めない:いかなる場合も含めません。
設定の優先順位
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 対応