0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Model Context Protocol完全解説 30日間シリーズ - Day 21【MCP応用 #21】設定ファイル管理:環境に応じた柔軟なMCP設定方法

Posted at

はじめに

この記事は、QiitaのModel Context Protocol(以下、MCP)解説シリーズの第21回です。
今回は、MCPサーバーを本番環境で運用する上で不可欠な、設定ファイル管理について解説します。開発環境、ステージング環境、本番環境といった異なる環境で、柔軟に設定を切り替えるためのベストプラクティスを学びましょう。

⚙️ なぜ設定ファイル管理が必要なのか?

アプリケーションの設定は環境によって大きく異なり、適切な管理が運用の成功を左右します。

環境別設定の例

  • 開発環境: ローカルのテスト用データベース、詳細なデバッグログ、開発用APIキー
  • ステージング環境: 本番に近い設定、テスト用の外部サービス、中程度のログレベル
  • 本番環境: 本物のデータベース、厳格なセキュリティ設定、最適化されたパフォーマンス設定

設定管理の課題

  • ハードコーディングの問題: 設定をコードに直接書くと環境切り替えが困難
  • 機密情報の漏洩リスク: APIキーやパスワードがバージョン管理システムに含まれる危険性
  • 設定の一貫性: 複数の開発者や環境での設定の違いによる問題
  • 運用時の設定変更: サーバー再起動なしに設定を変更したいケース

📝 実装:階層的設定管理システム

優先度の高い順に設定を適用する、階層的な設定管理システムを構築します。

  1. コマンドライン引数 (最高優先度)
  2. 環境変数
  3. 環境固有設定ファイル
  4. デフォルト設定ファイル (最低優先度)

ステップ1:プロジェクトのセットアップ

mkdir mcp-config-example
cd mcp-config-example
npm init -y
npm install @modelcontextprotocol/sdk dotenv zod yargs
npm install --save-dev typescript ts-node @types/node @types/yargs
npx tsc --init

ステップ2:設定スキーマの定義

src/config/schema.tsファイルを作成し、設定の型安全性を確保します。

import { z } from 'zod';

// ログレベルの定義
export const LogLevel = z.enum(['error', 'warn', 'info', 'debug', 'trace']);

// 環境タイプの定義  
export const Environment = z.enum(['development', 'staging', 'production', 'test']);

// データベース設定のスキーマ
export const DatabaseConfigSchema = z.object({
  url: z.string().url(),
  maxConnections: z.number().min(1).max(100).default(10),
  timeout: z.number().min(1000).default(5000),
  ssl: z.boolean().default(false),
});

// 外部API設定のスキーマ
export const ExternalApiConfigSchema = z.object({
  baseUrl: z.string().url(),
  apiKey: z.string().min(1),
  timeout: z.number().min(1000).default(10000),
  retries: z.number().min(0).max(5).default(3),
  rateLimit: z.object({
    requests: z.number().min(1).default(100),
    windowMs: z.number().min(1000).default(60000),
  }),
});

// サーバー設定のスキーマ
export const ServerConfigSchema = z.object({
  port: z.number().min(1).max(65535).default(3000),
  host: z.string().default('localhost'),
  cors: z.object({
    enabled: z.boolean().default(true),
    origins: z.array(z.string()).default(['*']),
  }),
});

// ログ設定のスキーマ
export const LogConfigSchema = z.object({
  level: LogLevel.default('info'),
  format: z.enum(['json', 'pretty']).default('pretty'),
  file: z.object({
    enabled: z.boolean().default(true),
    path: z.string().default('./logs'),
    maxSize: z.string().default('10MB'),
    maxFiles: z.number().default(5),
  }),
});

// メイン設定スキーマ
export const ConfigSchema = z.object({
  environment: Environment.default('development'),
  server: ServerConfigSchema,
  database: DatabaseConfigSchema,
  externalApi: ExternalApiConfigSchema,
  logging: LogConfigSchema,
  features: z.object({
    enableMetrics: z.boolean().default(false),
    enableAuth: z.boolean().default(false),
    enableRateLimit: z.boolean().default(true),
  }),
});

export type Config = z.infer<typeof ConfigSchema>;
export type LogLevel = z.infer<typeof LogLevel>;
export type Environment = z.infer<typeof Environment>;

ステップ3:設定ファイルの作成

各環境用の設定ファイルを作成します。

config/default.json

{
  "environment": "development",
  "server": {
    "port": 3000,
    "host": "localhost",
    "cors": {
      "enabled": true,
      "origins": ["http://localhost:3000"]
    }
  },
  "database": {
    "url": "sqlite://./data/development.db",
    "maxConnections": 5,
    "timeout": 5000,
    "ssl": false
  },
  "externalApi": {
    "baseUrl": "https://api-dev.example.com",
    "timeout": 10000,
    "retries": 3,
    "rateLimit": {
      "requests": 100,
      "windowMs": 60000
    }
  },
  "logging": {
    "level": "debug",
    "format": "pretty",
    "file": {
      "enabled": true,
      "path": "./logs",
      "maxSize": "10MB",
      "maxFiles": 5
    }
  },
  "features": {
    "enableMetrics": true,
    "enableAuth": false,
    "enableRateLimit": false
  }
}

config/development.json

{
  "server": {
    "port": 8080
  },
  "logging": {
    "level": "debug",
    "format": "pretty"
  },
  "features": {
    "enableMetrics": true,
    "enableAuth": false
  }
}

config/staging.json

{
  "environment": "staging",
  "server": {
    "port": 3000,
    "cors": {
      "origins": ["https://staging.example.com"]
    }
  },
  "database": {
    "url": "postgresql://user:password@staging-db.example.com:5432/mcp_staging",
    "maxConnections": 20,
    "ssl": true
  },
  "externalApi": {
    "baseUrl": "https://api-staging.example.com"
  },
  "logging": {
    "level": "info",
    "format": "json"
  },
  "features": {
    "enableMetrics": true,
    "enableAuth": true,
    "enableRateLimit": true
  }
}

config/production.json

{
  "environment": "production",
  "server": {
    "port": 443,
    "host": "0.0.0.0",
    "cors": {
      "origins": ["https://app.example.com"]
    }
  },
  "logging": {
    "level": "warn",
    "format": "json"
  },
  "features": {
    "enableMetrics": true,
    "enableAuth": true,
    "enableRateLimit": true
  }
}

ステップ4:設定ローダーの実装

src/config/loader.tsファイルを作成します。

import fs from 'fs';
import path from 'path';
import { config as loadEnv } from 'dotenv';
import { Config, ConfigSchema } from './schema.js';

export class ConfigLoader {
  private config: Config | null = null;
  private configDir: string;

  constructor(configDir: string = './config') {
    this.configDir = configDir;
  }

  /**
   * 設定を読み込む
   * @param environment 環境名
   * @param envFile 環境変数ファイルのパス
   */
  async load(environment?: string, envFile?: string): Promise<Config> {
    if (this.config) {
      return this.config;
    }

    // 1. 環境変数ファイルの読み込み
    if (envFile && fs.existsSync(envFile)) {
      loadEnv({ path: envFile });
    } else {
      loadEnv(); // デフォルトの .env ファイルを読み込み
    }

    // 2. 環境名の決定(優先度順)
    const env = environment || 
                process.env.NODE_ENV || 
                process.env.ENVIRONMENT || 
                'development';

    // 3. 設定ファイルの読み込み(階層的にマージ)
    const config = await this.loadConfigFiles(env);

    // 4. 環境変数による上書き
    const configWithEnv = this.applyEnvironmentVariables(config);

    // 5. バリデーション
    const validatedConfig = ConfigSchema.parse(configWithEnv);

    this.config = validatedConfig;
    return validatedConfig;
  }

  /**
   * 設定ファイルを階層的に読み込む
   */
  private async loadConfigFiles(environment: string): Promise<any> {
    const configFiles = [
      'default.json',
      `${environment}.json`,
      'local.json', // ローカル開発用(.gitignoreに追加推奨)
    ];

    let mergedConfig = {};

    for (const fileName of configFiles) {
      const filePath = path.join(this.configDir, fileName);
      if (fs.existsSync(filePath)) {
        try {
          const fileContent = fs.readFileSync(filePath, 'utf-8');
          const parsedConfig = JSON.parse(fileContent);
          mergedConfig = this.deepMerge(mergedConfig, parsedConfig);
        } catch (error) {
          throw new Error(`設定ファイル ${fileName} の読み込みに失敗しました: ${error}`);
        }
      }
    }

    return mergedConfig;
  }

  /**
   * 環境変数による設定の上書き
   */
  private applyEnvironmentVariables(config: any): any {
    const envMappings = {
      // サーバー設定
      'SERVER_PORT': 'server.port',
      'SERVER_HOST': 'server.host',
      
      // データベース設定
      'DATABASE_URL': 'database.url',
      'DATABASE_MAX_CONNECTIONS': 'database.maxConnections',
      'DATABASE_TIMEOUT': 'database.timeout',
      'DATABASE_SSL': 'database.ssl',
      
      // 外部API設定
      'EXTERNAL_API_BASE_URL': 'externalApi.baseUrl',
      'EXTERNAL_API_KEY': 'externalApi.apiKey',
      'EXTERNAL_API_TIMEOUT': 'externalApi.timeout',
      
      // ログ設定
      'LOG_LEVEL': 'logging.level',
      'LOG_FORMAT': 'logging.format',
      
      // 機能フラグ
      'ENABLE_METRICS': 'features.enableMetrics',
      'ENABLE_AUTH': 'features.enableAuth',
      'ENABLE_RATE_LIMIT': 'features.enableRateLimit',
    };

    const result = { ...config };

    Object.entries(envMappings).forEach(([envKey, configPath]) => {
      const envValue = process.env[envKey];
      if (envValue !== undefined) {
        this.setNestedProperty(result, configPath, this.parseEnvValue(envValue));
      }
    });

    return result;
  }

  /**
   * 環境変数の値を適切な型に変換
   */
  private parseEnvValue(value: string): any {
    // Boolean
    if (value.toLowerCase() === 'true') return true;
    if (value.toLowerCase() === 'false') return false;
    
    // Number
    if (/^\d+$/.test(value)) return parseInt(value, 10);
    if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
    
    // JSON
    if (value.startsWith('{') || value.startsWith('[')) {
      try {
        return JSON.parse(value);
      } catch {
        // JSON パースに失敗した場合はそのまま文字列として扱う
      }
    }
    
    return value;
  }

  /**
   * オブジェクトのディープマージ
   */
  private deepMerge(target: any, source: any): any {
    if (typeof target !== 'object' || typeof source !== 'object') {
      return source;
    }

    const result = { ...target };
    
    Object.keys(source).forEach(key => {
      if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
        result[key] = this.deepMerge(target[key] || {}, source[key]);
      } else {
        result[key] = source[key];
      }
    });

    return result;
  }

  /**
   * ネストされたプロパティに値を設定
   */
  private setNestedProperty(obj: any, path: string, value: any): void {
    const keys = path.split('.');
    let current = obj;
    
    for (let i = 0; i < keys.length - 1; i++) {
      if (!(keys[i] in current) || typeof current[keys[i]] !== 'object') {
        current[keys[i]] = {};
      }
      current = current[keys[i]];
    }
    
    current[keys[keys.length - 1]] = value;
  }

  /**
   * 現在の設定を取得
   */
  getConfig(): Config {
    if (!this.config) {
      throw new Error('設定が読み込まれていません。load() メソッドを先に実行してください。');
    }
    return this.config;
  }

  /**
   * 設定をリロード
   */
  async reload(environment?: string, envFile?: string): Promise<Config> {
    this.config = null;
    return this.load(environment, envFile);
  }
}

// シングルトンインスタンス
export const configLoader = new ConfigLoader();

ステップ5:コマンドライン引数の処理

src/cli.tsファイルを作成します。

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

export interface CliArgs {
  environment?: string;
  config?: string;
  port?: number;
  logLevel?: string;
  envFile?: string;
}

export function parseCliArgs(): CliArgs {
  return yargs(hideBin(process.argv))
    .option('environment', {
      alias: 'e',
      type: 'string',
      description: '実行環境 (development, staging, production)',
      choices: ['development', 'staging', 'production', 'test'],
    })
    .option('config', {
      alias: 'c',
      type: 'string',
      description: '設定ファイルのパス',
    })
    .option('port', {
      alias: 'p',
      type: 'number',
      description: 'サーバーポート番号',
    })
    .option('log-level', {
      alias: 'l',
      type: 'string',
      description: 'ログレベル',
      choices: ['error', 'warn', 'info', 'debug', 'trace'],
    })
    .option('env-file', {
      type: 'string',
      description: '環境変数ファイルのパス',
      default: '.env',
    })
    .help()
    .parseSync();
}

ステップ6:MCPサーバーの実装

src/server.tsファイルを作成し、設定システムを統合します。

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { configLoader } from './config/loader.js';
import { parseCliArgs } from './cli.js';
import { Config } from './config/schema.js';

class ConfigurableMCPServer {
  private server: Server;
  private config: Config;

  constructor(config: Config) {
    this.config = config;
    this.server = new Server(
      {
        name: 'configurable-mcp-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // ツール一覧の取得
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: 'get_config',
            description: '現在の設定情報を取得する',
            inputSchema: {
              type: 'object',
              properties: {
                section: {
                  type: 'string',
                  description: '取得したい設定セクション (server, database, logging等)',
                  enum: ['server', 'database', 'externalApi', 'logging', 'features', 'all'],
                },
              },
            },
          },
          {
            name: 'external_api_call',
            description: '外部APIを呼び出す(設定されたAPIキーを使用)',
            inputSchema: {
              type: 'object',
              properties: {
                endpoint: {
                  type: 'string',
                  description: 'APIエンドポイント',
                },
                method: {
                  type: 'string',
                  enum: ['GET', 'POST', 'PUT', 'DELETE'],
                  default: 'GET',
                },
              },
              required: ['endpoint'],
            },
          },
          {
            name: 'database_query',
            description: 'データベースにクエリを実行する(設定された接続情報を使用)',
            inputSchema: {
              type: 'object',
              properties: {
                query: {
                  type: 'string',
                  description: 'SQL クエリ',
                },
              },
              required: ['query'],
            },
          },
        ],
      };
    });

    // ツールの実行
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name: toolName, arguments: args } = request.params;

      try {
        let result;
        switch (toolName) {
          case 'get_config':
            result = this.getConfigInfo(args as { section?: string });
            break;
          case 'external_api_call':
            result = await this.callExternalApi(args as { endpoint: string; method?: string });
            break;
          case 'database_query':
            result = await this.executeDatabaseQuery(args as { query: string });
            break;
          default:
            throw new Error(`Unknown tool: ${toolName}`);
        }

        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(result, null, 2),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error: ${(error as Error).message}`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  private getConfigInfo(args: { section?: string }) {
    const section = args.section || 'all';
    
    if (section === 'all') {
      // 機密情報をマスクして返す
      const safeConfig = { ...this.config };
      if (safeConfig.externalApi.apiKey) {
        safeConfig.externalApi.apiKey = safeConfig.externalApi.apiKey.substring(0, 8) + '...';
      }
      return { config: safeConfig };
    }

    const sectionConfig = (this.config as any)[section];
    if (!sectionConfig) {
      throw new Error(`Unknown config section: ${section}`);
    }

    return { [section]: sectionConfig };
  }

  private async callExternalApi(args: { endpoint: string; method?: string }) {
    const { baseUrl, apiKey, timeout } = this.config.externalApi;
    const url = `${baseUrl}${args.endpoint}`;
    const method = args.method || 'GET';

    console.log(`Calling external API: ${method} ${url}`);
    console.log(`Using API key: ${apiKey.substring(0, 8)}...`);
    console.log(`Timeout: ${timeout}ms`);

    // 実際のAPI呼び出しをシミュレート
    return {
      message: `Simulated ${method} request to ${url}`,
      config: {
        timeout,
        retries: this.config.externalApi.retries,
      },
    };
  }

  private async executeDatabaseQuery(args: { query: string }) {
    const { url, maxConnections, timeout, ssl } = this.config.database;
    
    console.log(`Executing query on database: ${url}`);
    console.log(`Max connections: ${maxConnections}`);
    console.log(`Timeout: ${timeout}ms`);
    console.log(`SSL: ${ssl}`);

    // 実際のデータベースクエリをシミュレート
    return {
      message: `Simulated query execution: ${args.query}`,
      config: {
        maxConnections,
        timeout,
        ssl,
      },
    };
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    
    console.log(`MCP Server started with environment: ${this.config.environment}`);
    console.log(`Server config: ${this.config.server.host}:${this.config.server.port}`);
    console.log(`Log level: ${this.config.logging.level}`);
    console.log(`Features: ${JSON.stringify(this.config.features)}`);
  }
}

// メイン関数
async function main() {
  try {
    // コマンドライン引数の解析
    const cliArgs = parseCliArgs();
    
    // 設定の読み込み
    const config = await configLoader.load(cliArgs.environment, cliArgs.envFile);
    
    // CLIからの設定上書き
    if (cliArgs.port) {
      config.server.port = cliArgs.port;
    }
    if (cliArgs.logLevel) {
      config.logging.level = cliArgs.logLevel as any;
    }

    // サーバー起動
    const server = new ConfigurableMCPServer(config);
    await server.start();
  } catch (error) {
    console.error('サーバーの起動に失敗しました:', error);
    process.exit(1);
  }
}

main();

🚀 実行方法と動作確認

基本的な実行

# 開発環境で起動
npm run dev

# または明示的に環境を指定
npx ts-node src/server.ts --environment development

# 本番環境で起動
npx ts-node src/server.ts --environment production

# カスタム設定ファイルを使用
npx ts-node src/server.ts --config ./custom-config.json

# ポートを上書き
npx ts-node src/server.ts --port 9000

# 環境変数ファイルを指定
npx ts-node src/server.ts --env-file .env.staging

環境変数による設定上書き

# 環境変数でAPIキーを設定
EXTERNAL_API_KEY=prod_secret_key_xyz npx ts-node src/server.ts --environment production

# 複数の設定を環境変数で上書き
DATABASE_URL=postgresql://localhost:5432/mydb \
LOG_LEVEL=debug \
ENABLE_METRICS=true \
npx ts-node src/server.ts

📋 環境変数ファイルの活用

.env.development

NODE_ENV=development
LOG_LEVEL=debug
DATABASE_URL=sqlite://./data/dev.db
EXTERNAL_API_KEY=dev_api_key_123
ENABLE_METRICS=true
ENABLE_AUTH=false

.env.production

NODE_ENV=production
LOG_LEVEL=warn
DATABASE_SSL=true
ENABLE_METRICS=true
ENABLE_AUTH=true
ENABLE_RATE_LIMIT=true
# 機密情報は実際の運用では外部シークレット管理システムから取得

🔒 セキュリティのベストプラクティス

機密情報の管理

// src/config/secrets.ts
import fs from 'fs';

export class SecretManager {
  /**
   * Kubernetes Secrets からの読み込み
   */
  static loadFromKubernetesSecrets(secretName: string): string {
    const secretPath = `/var/run/secrets/kubernetes.io/serviceaccount/${secretName}`;
    if (fs.existsSync(secretPath)) {
      return fs.readFileSync(secretPath, 'utf-8').trim();
    }
    throw new Error(`Secret not found: ${secretName}`);
  }

  /**
   * AWS Secrets Manager からの読み込み(非同期)
   */
  static async loadFromAWSSecretsManager(secretId: string): Promise<string> {
    // AWS SDK を使用した実装例
    // const client = new SecretsManagerClient({ region: 'us-east-1' });
    // const response = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
    // return response.SecretString || '';
    throw new Error('AWS Secrets Manager integration not implemented');
  }

  /**
   * 環境変数の検証(機密情報の漏洩を防ぐ)
   */
  static validateSensitiveEnvVars() {
    const sensitiveKeys = ['API_KEY', 'DATABASE_PASSWORD', 'JWT_SECRET'];
    
    sensitiveKeys.forEach(key => {
      const value = process.env[key];
      if (value) {
        // 開発環境でのみ警告
        if (process.env.NODE_ENV === 'development' && value.includes('prod')) {
          console.warn(`⚠️  Production credential detected in development: ${key}`);
        }
      }
    });
  }
}

設定の検証とサニタイズ

// src/config/validator.ts
export class ConfigValidator {
  /**
   * URL の安全性チェック
   */
  static validateUrl(url: string): boolean {
    try {
      const parsed = new URL(url);
      // 内部IPアドレスへのアクセスを制限
      const hostname = parsed.hostname;
      if (hostname === 'localhost' || 
          hostname.startsWith('127.') || 
          hostname.startsWith('10.') ||
          hostname.startsWith('172.16.') ||
          hostname.startsWith('192.168.')) {
        if (process.env.NODE_ENV === 'production') {
          throw new Error(`Internal IP access not allowed in production: ${hostname}`);
        }
      }
      return true;
    } catch (error) {
      throw new Error(`Invalid URL: ${url}`);
    }
  }

  /**
   * 設定値の範囲チェック
   */
  static validateResourceLimits(config: Config): void {
    // 接続数の上限チェック
    if (config.database.maxConnections > 100) {
      throw new Error('Database max connections cannot exceed 100');
    }

    // タイムアウトの範囲チェック
    if (config.externalApi.timeout > 30000) {
      console.warn('⚠️  External API timeout is very high:', config.externalApi.timeout);
    }
  }
}

🎯 設定管理のベストプラクティス

1. 階層的な設定の優先順位

コマンドライン引数 > 環境変数 > 環境固有設定ファイル > デフォルト設定ファイル

この優先順位により、柔軟で安全な設定管理が可能になります。

2. 機密情報の取り扱い

// ❌ 悪い例:機密情報をコードに含める
const apiKey = 'prod_secret_key_12345';

// ✅ 良い例:環境変数から取得
const apiKey = process.env.EXTERNAL_API_KEY || '';
if (!apiKey) {
  throw new Error('EXTERNAL_API_KEY environment variable is required');
}

// ✅ さらに良い例:外部シークレット管理システムを使用
const apiKey = await SecretManager.loadFromVault('external-api-key');

3. 設定ファイルの分離

config/
├── default.json          # 基本設定
├── development.json      # 開発環境固有
├── staging.json          # ステージング環境固有
├── production.json       # 本番環境固有
├── test.json            # テスト環境固有
└── local.json           # 個人用設定(gitignore対象)

4. 型安全性の確保

// 設定の型安全性により、実行時エラーを防ぐ
const config = await configLoader.load();

// TypeScriptが型をチェック
config.server.port; // number型
config.logging.level; // 'error' | 'warn' | 'info' | 'debug' | 'trace'
config.features.enableAuth; // boolean型

🔧 高度な設定管理機能

動的設定の再読み込み

src/config/watcher.tsファイルを作成し、設定ファイルの変更を監視します。

import fs from 'fs';
import path from 'path';
import { EventEmitter } from 'events';
import { configLoader } from './loader.js';

export class ConfigWatcher extends EventEmitter {
  private watchers: fs.FSWatcher[] = [];
  private configDir: string;

  constructor(configDir: string = './config') {
    super();
    this.configDir = configDir;
  }

  /**
   * 設定ファイルの監視を開始
   */
  startWatching() {
    const configFiles = [
      'default.json',
      'development.json',
      'staging.json',
      'production.json',
      'local.json',
    ];

    configFiles.forEach(fileName => {
      const filePath = path.join(this.configDir, fileName);
      if (fs.existsSync(filePath)) {
        const watcher = fs.watch(filePath, (eventType) => {
          if (eventType === 'change') {
            this.handleConfigChange(fileName);
          }
        });
        this.watchers.push(watcher);
      }
    });

    console.log(`設定ファイルの監視を開始しました: ${this.configDir}`);
  }

  /**
   * 監視を停止
   */
  stopWatching() {
    this.watchers.forEach(watcher => watcher.close());
    this.watchers = [];
    console.log('設定ファイルの監視を停止しました');
  }

  /**
   * 設定変更時の処理
   */
  private async handleConfigChange(fileName: string) {
    try {
      console.log(`設定ファイルが変更されました: ${fileName}`);
      
      // 設定を再読み込み
      const newConfig = await configLoader.reload();
      
      // 変更イベントを発火
      this.emit('configChanged', { fileName, config: newConfig });
      
      console.log('設定の再読み込みが完了しました');
    } catch (error) {
      console.error('設定の再読み込みに失敗しました:', error);
      this.emit('configError', error);
    }
  }
}

設定の検証とテスト

src/config/validator.test.tsファイルを作成します。

import { ConfigSchema } from './schema.js';
import { ConfigValidator } from './validator.js';

describe('Configuration Validation', () => {
  test('有効な設定が正しく検証される', () => {
    const validConfig = {
      environment: 'development',
      server: {
        port: 3000,
        host: 'localhost',
        cors: {
          enabled: true,
          origins: ['http://localhost:3000'],
        },
      },
      database: {
        url: 'sqlite://./test.db',
        maxConnections: 10,
        timeout: 5000,
        ssl: false,
      },
      externalApi: {
        baseUrl: 'https://api.example.com',
        apiKey: 'test-key',
        timeout: 10000,
        retries: 3,
        rateLimit: {
          requests: 100,
          windowMs: 60000,
        },
      },
      logging: {
        level: 'info',
        format: 'json',
        file: {
          enabled: true,
          path: './logs',
          maxSize: '10MB',
          maxFiles: 5,
        },
      },
      features: {
        enableMetrics: true,
        enableAuth: false,
        enableRateLimit: true,
      },
    };

    expect(() => ConfigSchema.parse(validConfig)).not.toThrow();
  });

  test('無効な設定でエラーが発生する', () => {
    const invalidConfig = {
      server: {
        port: 'not-a-number', // 数値である必要がある
      },
    };

    expect(() => ConfigSchema.parse(invalidConfig)).toThrow();
  });

  test('URL検証が正しく動作する', () => {
    expect(() => ConfigValidator.validateUrl('https://api.example.com')).not.toThrow();
    expect(() => ConfigValidator.validateUrl('invalid-url')).toThrow();
  });
});

🐳 Docker環境での設定管理

Dockerfile

FROM node:18-alpine

WORKDIR /app

# 依存関係のインストール
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションコードのコピー
COPY . .

# TypeScriptのビルド
RUN npm run build

# 設定ファイル用のボリュームマウントポイント
VOLUME ["/app/config"]

# 環境変数のデフォルト値
ENV NODE_ENV=production
ENV LOG_LEVEL=info

# 非rootユーザーで実行
USER node

EXPOSE 3000

CMD ["node", "dist/server.js"]

docker-compose.yml

version: '3.8'

services:
  mcp-server:
    build: .
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/mcp_prod
      - EXTERNAL_API_KEY=${EXTERNAL_API_KEY}
      - LOG_LEVEL=info
      - ENABLE_METRICS=true
    volumes:
      - ./config:/app/config:ro
      - ./logs:/app/logs
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=mcp_prod
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

⚡ パフォーマンス最適化

設定キャッシュの実装

// src/config/cache.ts
export class ConfigCache {
  private static cache = new Map<string, any>();
  private static ttl = new Map<string, number>();
  private static readonly DEFAULT_TTL = 5 * 60 * 1000; // 5分

  static set(key: string, value: any, ttlMs?: number): void {
    this.cache.set(key, value);
    this.ttl.set(key, Date.now() + (ttlMs || this.DEFAULT_TTL));
  }

  static get<T>(key: string): T | undefined {
    const expiry = this.ttl.get(key);
    if (expiry && Date.now() > expiry) {
      this.cache.delete(key);
      this.ttl.delete(key);
      return undefined;
    }
    return this.cache.get(key) as T;
  }

  static clear(): void {
    this.cache.clear();
    this.ttl.clear();
  }

  static has(key: string): boolean {
    return this.cache.has(key) && (this.ttl.get(key) || 0) > Date.now();
  }
}

📊 設定の監査とログ

設定変更の追跡

// src/config/audit.ts
interface ConfigChangeEvent {
  timestamp: Date;
  source: 'file' | 'env' | 'cli';
  path: string;
  oldValue: any;
  newValue: any;
  user?: string;
}

export class ConfigAuditor {
  private changes: ConfigChangeEvent[] = [];
  private readonly maxEntries = 1000;

  logChange(event: Omit<ConfigChangeEvent, 'timestamp'>): void {
    this.changes.push({
      ...event,
      timestamp: new Date(),
    });

    // 古いエントリを削除
    if (this.changes.length > this.maxEntries) {
      this.changes.shift();
    }

    console.log('設定変更:', {
      path: event.path,
      source: event.source,
      timestamp: new Date().toISOString(),
    });
  }

  getChangeHistory(): ConfigChangeEvent[] {
    return [...this.changes];
  }

  exportAuditLog(): string {
    return JSON.stringify(this.changes, null, 2);
  }
}

🎯 まとめ:設定管理のベストプラクティス

設計原則

  1. 設定の階層化: デフォルト値から環境固有の設定まで段階的に適用
  2. 型安全性: スキーマ検証により実行時エラーを防止
  3. 機密情報の分離: APIキーなどは環境変数や外部シークレット管理システムを使用
  4. 環境の分離: 開発・ステージング・本番で適切に設定を分離
  5. 監査可能性: 設定変更の履歴を追跡可能にする

運用上の注意点

  • 設定ファイルのバックアップ: 重要な設定変更前にはバックアップを作成
  • 段階的なデプロイ: 設定変更は段階的に適用し、問題があれば即座にロールバック
  • 監視とアラート: 設定に起因する問題を早期発見するための監視体制
  • ドキュメント化: 各設定項目の目的と影響範囲を明確に文書化

セキュリティチェックリスト

  • 機密情報がバージョン管理システムに含まれていない
  • 本番環境の認証情報が開発環境で使用されていない
  • 設定ファイルの権限が適切に設定されている
  • ログに機密情報が出力されていない
  • 外部からの設定変更を適切に制限している

この包括的な設定管理システムにより、MCPサーバーを様々な環境で安全かつ効率的に運用できます。設定の変更や環境の切り替えが簡単になり、運用時のミスを大幅に削減できるでしょう。

次回は、Day 22で、Dockerを使ったMCPサーバーの配布方法について詳しく解説します。今回学んだ設定管理の知識は、コンテナ化においても重要な基盤となります。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?