フィーチャーフラグ設計 - 安全なリリース戦略

15分 read | 2025.01.10

フィーチャーフラグとは

フィーチャーフラグ(機能フラグ)は、コードをデプロイした後でも機能のオン/オフを切り替えられる仕組みです。リスクを最小化しながら新機能をリリースできます。

flowchart TB
    subgraph Deploy["デプロイ済みコード"]
        Code["新機能コード"]
        Flag["フラグ判定"]
        Code --> Flag
    end

    Flag -->|ON| NewFeature["新機能表示"]
    Flag -->|OFF| OldFeature["既存機能表示"]

    Config["フラグ設定<br/>(管理画面)"] -.->|制御| Flag

フラグの種類

種類用途寿命
リリースフラグ新機能の段階的公開短期(数週間)
実験フラグA/Bテスト中期(数ヶ月)
運用フラグ機能の緊急停止長期
権限フラグユーザー種別による制御永続

実装パターン

シンプルな実装

// フラグ設定
const featureFlags = {
  newCheckout: true,
  darkMode: false,
  betaFeatures: process.env.NODE_ENV === 'development',
};

// 使用箇所
function CheckoutPage() {
  if (featureFlags.newCheckout) {
    return <NewCheckout />;
  }
  return <LegacyCheckout />;
}

コンテキストベース実装

interface FeatureFlagContext {
  userId: string;
  userRole: 'free' | 'premium' | 'enterprise';
  country: string;
  percentage?: number;
}

class FeatureFlagService {
  private flags: Map<string, FlagRule[]> = new Map();

  isEnabled(flagName: string, context: FeatureFlagContext): boolean {
    const rules = this.flags.get(flagName);
    if (!rules) return false;

    for (const rule of rules) {
      if (this.evaluateRule(rule, context)) {
        return true;
      }
    }
    return false;
  }

  private evaluateRule(rule: FlagRule, context: FeatureFlagContext): boolean {
    // ユーザーロールチェック
    if (rule.roles && !rule.roles.includes(context.userRole)) {
      return false;
    }

    // パーセンテージロールアウト
    if (rule.percentage !== undefined) {
      const hash = this.hashUserId(context.userId);
      return hash < rule.percentage;
    }

    return true;
  }

  private hashUserId(userId: string): number {
    // 0-100の一貫したハッシュ値を生成
    let hash = 0;
    for (let i = 0; i < userId.length; i++) {
      hash = ((hash << 5) - hash) + userId.charCodeAt(i);
      hash |= 0;
    }
    return Math.abs(hash) % 100;
  }
}

Reactでの使用

// フラグプロバイダー
const FeatureFlagContext = createContext<FeatureFlagService | null>(null);

export function useFeatureFlag(flagName: string): boolean {
  const service = useContext(FeatureFlagContext);
  const user = useCurrentUser();

  if (!service || !user) return false;

  return service.isEnabled(flagName, {
    userId: user.id,
    userRole: user.role,
    country: user.country,
  });
}

// コンポーネントで使用
function Dashboard() {
  const showNewAnalytics = useFeatureFlag('new-analytics');

  return (
    <div>
      {showNewAnalytics ? <NewAnalytics /> : <LegacyAnalytics />}
    </div>
  );
}

段階的ロールアウト

// 1% → 5% → 25% → 50% → 100% と段階的に公開
const rolloutConfig = {
  'new-payment': {
    stages: [
      { percentage: 1, startDate: '2025-01-10' },
      { percentage: 5, startDate: '2025-01-12' },
      { percentage: 25, startDate: '2025-01-15' },
      { percentage: 50, startDate: '2025-01-20' },
      { percentage: 100, startDate: '2025-01-25' },
    ],
  },
};

運用ベストプラクティス

1. フラグの命名規則

// 良い例
'checkout-new-payment-form'
'experiment-pricing-page-v2'
'ops-maintenance-mode'

// 悪い例
'flag1'
'new_feature'
'test'

2. 技術的負債の管理

// フラグに期限を設定
interface FeatureFlag {
  name: string;
  enabled: boolean;
  expiresAt: Date;  // 期限切れアラート
  owner: string;    // 責任者
}

// 定期的なクリーンアップ
function auditFlags(flags: FeatureFlag[]) {
  const expiredFlags = flags.filter(f => f.expiresAt < new Date());
  if (expiredFlags.length > 0) {
    notifyOwners(expiredFlags);
  }
}

3. 緊急停止(キルスイッチ)

// 即座に機能を停止できる仕組み
async function killSwitch(flagName: string) {
  await flagService.disable(flagName);
  await invalidateCache(flagName);
  await notifyTeam(`${flagName} disabled via kill switch`);
}

現場での失敗例と対策

  1. フラグの削除忘れ → 期限設定と定期レビュー
  2. テスト環境との不整合 → 環境別設定の明確化
  3. パフォーマンス低下 → フラグ評価のキャッシュ

関連記事

まとめ

フィーチャーフラグは、リリースリスクを軽減する強力なツールです。適切な設計と運用ルールを設けることで、安全かつ高速なリリースサイクルを実現できます。

← Back to list