← ブログ一覧

Feature Flag による段階的リリース実践ガイド

機能フラグで本番リスクを最小化。カナリアリリース・A/Bテスト・ロールバックを実装する手順を、LaunchDarkly・Unleash の比較、実装パターン、運用設計まで網羅して解説します。

#DevOps#CI/CD#アーキテクチャ#Next.js
Feature Flag による段階的リリース実践ガイド

Feature Flag による段階的リリース実践ガイド

「新機能をリリースしたら本番で予期しないエラーが…」「全ユーザーに影響が出る前に戻したい」。こうした事態を避けるため、Feature Flag(機能フラグ) による段階的リリースが注目されています。

本記事では、受託開発・自社開発の現場で即使える Feature Flag の実装方法を、ツール比較・実装パターン・運用設計まで具体例とともに解説します。


1. Feature Flag とは何か

1-1. 基本概念

Feature Flag(フィーチャーフラグ)は、コードをデプロイせずに機能の ON/OFF を切り替える仕組みです。

従来のリリース

  • コード変更 → デプロイ → 全ユーザーに即座に公開
  • 問題発生時は再デプロイが必要

Feature Flag を使ったリリース

  • コードは先にデプロイ(フラグは OFF)
  • 管理画面でフラグを ON にして段階的に公開
  • 問題発生時はフラグを OFF にするだけ(即座にロールバック)

1-2. 主なユースケース

| ユースケース | 説明 | 適用場面 | |------------|------|--------| | カナリアリリース | 一部ユーザーに先行公開してリスクを検証 | 大規模機能の本番投入前 | | A/Bテスト | 異なるUIや機能を比較して効果測定 | 新UI/UX の検証 | | 段階的ロールアウト | 1% → 10% → 50% → 100% と徐々に公開範囲を拡大 | リスクを最小化したい重要機能 | | Kill Switch | 問題発生時に即座に機能を無効化 | 決済・外部API連携など | | 環境別有効化 | 開発/ステージング環境でのみ機能を有効化 | 開発中機能の検証 |


2. Feature Flag サービスの比較

2-1. 主要サービスの特徴

| サービス | 料金 | 主な特徴 | 適した規模 | |---------|------|---------|----------| | LaunchDarkly | $10/seat〜 | 大規模向け、豊富な機能、SDKが充実 | エンタープライズ | | Unleash | OSS/有料 | セルフホスト可能、コスト抑制 | 中小〜大規模 | | Flagsmith | OSS/有料 | UI/UXが優れている、手軽に開始 | スタートアップ〜中規模 | | PostHog | OSS/有料 | アナリティクス統合、A/Bテスト機能 | プロダクト分析も必要な場合 | | 自前実装 | 無料(開発コスト) | 完全カスタマイズ可能 | 小規模MVP、学習目的 |

2-2. 選定基準チェックリスト

必須要件

  • [ ] SDK が使用言語・フレームワークに対応しているか
  • [ ] 段階的ロールアウト(パーセンテージベース)をサポートしているか
  • [ ] 管理画面で非エンジニアも操作可能か
  • [ ] 監査ログ・変更履歴が記録されるか

推奨要件

  • [ ] ターゲティング(ユーザー属性による出し分け)が可能か
  • [ ] A/Bテスト機能があるか
  • [ ] 既存の分析ツール(GA4、Amplitudeなど)と連携できるか
  • [ ] セルフホストできるか(セキュリティ要件が厳しい場合)

3. 実装パターン:Next.js での例

3-1. 環境構築

本記事では OSS の Unleash を例に実装を進めます。

# Unleash サーバーを Docker で起動
docker run -d \
  -p 4242:4242 \
  -e DATABASE_URL=postgres://unleash:password@postgres/unleash \
  unleashorg/unleash-server:latest

# Next.js プロジェクトに SDK をインストール
npm install @unleash/nextjs @unleash/proxy-client-react

3-2. サーバーサイドでのフラグ評価

// app/api/unleash/route.ts
import { getDefinitions } from '@unleash/nextjs';

export async function GET() {
  const definitions = await getDefinitions({
    unleashUrl: process.env.UNLEASH_SERVER_URL!,
    unleashApiToken: process.env.UNLEASH_API_TOKEN!,
    appName: 'my-app',
  });
  
  return Response.json(definitions);
}
// app/products/page.tsx (Server Component)
import { evaluateFlags } from '@unleash/nextjs';

export default async function ProductsPage() {
  const flags = await evaluateFlags({
    unleashUrl: process.env.UNLEASH_SERVER_URL!,
    unleashApiToken: process.env.UNLEASH_API_TOKEN!,
    appName: 'my-app',
    context: {
      userId: 'anonymous',
    },
  });

  const showNewProductUI = flags.isEnabled('new-product-ui');

  if (showNewProductUI) {
    return <NewProductList />;
  }

  return <LegacyProductList />;
}

3-3. クライアントサイドでのフラグ評価

// app/providers.tsx
'use client';
import { FlagProvider } from '@unleash/proxy-client-react';

const config = {
  url: '/api/unleash',
  clientKey: process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY!,
  refreshInterval: 30, // 30秒ごとにフラグを再取得
  appName: 'my-app',
};

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <FlagProvider config={config}>
      {children}
    </FlagProvider>
  );
}
// app/checkout/page.tsx (Client Component)
'use client';
import { useFlag } from '@unleash/proxy-client-react';

export default function CheckoutPage() {
  const showNewCheckout = useFlag('new-checkout-flow');

  if (showNewCheckout) {
    return <NewCheckoutFlow />;
  }

  return <LegacyCheckoutFlow />;
}

3-4. ユーザー属性によるターゲティング

// 特定のユーザーにだけ新機能を公開
const flags = await evaluateFlags({
  unleashUrl: process.env.UNLEASH_SERVER_URL!,
  unleashApiToken: process.env.UNLEASH_API_TOKEN!,
  appName: 'my-app',
  context: {
    userId: session.user.id,
    properties: {
      email: session.user.email,
      plan: session.user.plan, // 'free' | 'pro' | 'enterprise'
      betaTester: session.user.betaTester,
    },
  },
});

const showPremiumFeature = flags.isEnabled('premium-analytics');

Unleash 管理画面での設定例

  • Strategy: userWithId(特定ユーザーのみ)
  • Strategy: gradualRollout(10%のユーザーに公開)
  • Strategy: flexibleRollout + Constraint(plan = 'pro' のユーザーのみ)

4. 段階的ロールアウトの実践手順

4-1. フェーズ設計

| フェーズ | 公開範囲 | 期間 | 判断基準 | |---------|---------|------|--------| | 1. 内部テスト | 開発チーム(5名) | 1日 | 致命的なバグがないか | | 2. ベータユーザー | ベータテスター(50名) | 3日 | エラー率 < 0.1%、主要機能が動作 | | 3. カナリア | 全体の 5% | 2日 | エラー率・パフォーマンス監視 | | 4. 段階拡大 | 10% → 25% → 50% | 各2日 | ビジネス指標(CVRなど)を確認 | | 5. 全体公開 | 100% | - | 問題なければ完全移行 |

4-2. 監視指標の設定

// フラグ評価時にメトリクスを送信
import { track } from '@/lib/analytics';

const flags = await evaluateFlags(context);
const showNewUI = flags.isEnabled('new-ui');

track('feature_flag_evaluated', {
  flagName: 'new-ui',
  enabled: showNewUI,
  userId: context.userId,
  timestamp: new Date().toISOString(),
});

監視すべき指標

  • エラー率(フラグ ON グループ vs OFF グループ)
  • レスポンスタイム(P50、P95、P99)
  • ビジネス指標(CVR、離脱率、DAUなど)
  • ユーザーフィードバック(サポート問い合わせ数)

4-3. ロールバック手順

# 問題発生時の緊急対応フロー

# 1. Unleash 管理画面でフラグを即座に OFF
#    → 全ユーザーが旧機能に戻る(数秒〜数十秒)

# 2. エラーログ・メトリクスを確認
#    → どの条件で問題が発生したかを特定

# 3. 修正版をデプロイ
#    → フラグは OFF のまま(影響なし)

# 4. 内部テストで検証後、再度段階的にロールアウト

5. A/Bテストの実装

5-1. バリアント(Variant)の定義

// Unleash でバリアントを設定
const flags = await evaluateFlags(context);
const variant = flags.getVariant('checkout-button-color');

const buttonColor = {
  blue: '#3B82F6',
  green: '#10B981',
  red: '#EF4444',
}[variant.name] || '#3B82F6'; // デフォルトは青

return (
  <button
    style={{ backgroundColor: buttonColor }}
    onClick={handleCheckout}
  >
    購入する
  </button>
);

5-2. 結果の計測

// app/api/track/route.ts
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';

export async function POST(req: NextRequest) {
  const { event, userId, variant, properties } = await req.json();

  await db.insert('analytics_events').values({
    event_name: event,
    user_id: userId,
    variant_name: variant,
    properties: properties,
    created_at: new Date(),
  });

  return Response.json({ success: true });
}
-- 購入率の比較クエリ
SELECT
  variant_name,
  COUNT(DISTINCT CASE WHEN event_name = 'checkout_viewed' THEN user_id END) AS views,
  COUNT(DISTINCT CASE WHEN event_name = 'purchase_completed' THEN user_id END) AS conversions,
  ROUND(
    100.0 * COUNT(DISTINCT CASE WHEN event_name = 'purchase_completed' THEN user_id END) /
    NULLIF(COUNT(DISTINCT CASE WHEN event_name = 'checkout_viewed' THEN user_id END), 0),
    2
  ) AS conversion_rate
FROM analytics_events
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY variant_name;

6. 運用設計のポイント

6-1. フラグのライフサイクル管理

| フェーズ | 期間目安 | 対応 | |---------|---------|------| | 開発中 | 1〜2週間 | フラグ OFF、開発環境でのみ ON | | ロールアウト中 | 1〜2週間 | 段階的に公開範囲を拡大 | | 安定稼働 | 2週間 | 100% 公開、問題なければ次フェーズへ | | クリーンアップ | - | フラグを削除、コードから条件分岐を除去 |

重要:古いフラグの削除

// ❌ 悪い例:フラグが残り続ける
if (flags.isEnabled('new-ui-2023')) {
  // 2年前の実験が残ったまま…
}

// ✅ 良い例:定期的にクリーンアップ
// 1. Unleash で使用されていないフラグを確認
// 2. コードから該当の条件分岐を削除
// 3. Unleash からフラグを削除

6-2. チームでの運用ルール

フラグ命名規則

// ✅ 良い例
'checkout-redesign-2024-q2'  // 機能名-年度-四半期
'payment-provider-stripe'     // 機能名-具体的な識別子
'experiment-button-color-ab'  // 実験であることを明示

// ❌ 悪い例
'new-feature'  // 何の機能か不明
'test123'      // 一時的なテスト用?
'flag1'        // 意味不明

ドキュメント化テンプレート

## フラグ名: `checkout-redesign-2024-q2`

### 目的
決済フローを簡素化し、カート放棄率を 20% 削減する

### 対象ユーザー
- フェーズ1: ベータテスター(betaTester = true)
- フェーズ2: 全ユーザーの 5% → 10% → 50% → 100%

### 監視指標
- 決済完了率(目標: 70% 以上)
- エラー率(許容: 0.1% 以下)
- P95 レスポンスタイム(許容: 1秒以内)

### ロールバック基準
- 決済完了率が 60% を下回る
- エラー率が 0.5% を超える
- 決済関連の問い合わせが急増

### クリーンアップ予定
2024年8月末(安定稼働後1ヶ月)

6-3. セキュリティ考慮事項

フラグ情報の扱い

// ❌ 悪い例:機密情報をフラグ名に含める
'premium-pricing-50percent-off'  // 割引率が露出
'beta-feature-credit-card-token' // 機密用語

// ✅ 良い例:抽象的な名前にする
'premium-pricing-experiment'
'payment-method-alternative'

API トークンの管理

# 環境変数で管理(.env.local)
UNLEASH_SERVER_URL=https://unleash.example.com
UNLEASH_API_TOKEN=*:production.xxxxxxxxxxxxx
NEXT_PUBLIC_UNLEASH_CLIENT_KEY=your-client-key

# .env.local は .gitignore に追加
echo ".env.local" >> .gitignore

7. トラブルシューティング

7-1. よくある問題と対処法

| 問題 | 原因 | 対処法 | |------|------|--------| | フラグの変更が反映されない | キャッシュが残っている | refreshInterval を短くする、手動で updateContext() を呼ぶ | | ユーザーごとに挙動がバラバラ | userId が毎回変わる | セッション ID を使う、Cookie に保存 | | A/Bテストで片方に偏る | バリアント配分が不均等 | Unleash で stickinessuserId に設定 | | 本番で意図しない機能が有効 | 環境別設定の誤り | 環境ごとに Unleash プロジェクトを分ける |

7-2. デバッグ用コード

// 開発環境でフラグの状態を確認
if (process.env.NODE_ENV === 'development') {
  console.table({
    'new-ui': flags.isEnabled('new-ui'),
    'premium-feature': flags.isEnabled('premium-feature'),
    'experiment-variant': flags.getVariant('experiment').name,
  });
}

まとめ

Feature Flag による段階的リリースは、本番環境でのリスクを最小化しながら、迅速に価値を届けるための強力な手法です。

本記事で解説した内容

  • Feature Flag の基本概念とユースケース
  • LaunchDarkly・Unleash などのサービス比較
  • Next.js での実装パターン(Server/Client Components)
  • 段階的ロールアウトの実践手順
  • A/Bテストの実装と計測
  • 運用設計(フラグのライフサイクル、命名規則、セキュリティ)
  • トラブルシューティング

導入のステップ

  1. 小さく始める:1つの機能でフラグを試す
  2. 監視体制を整える:エラー率・パフォーマンスを計測
  3. チームで運用ルールを決める:命名規則・クリーンアップ基準
  4. 段階的に適用範囲を広げる:重要な機能から順に

Feature Flag は、技術的負債になりやすい側面もあります。定期的なクリーンアップ明確な運用ルールを設けることで、健全な開発サイクルを維持できます。


Yureate では、Feature Flag を活用した安全なリリース戦略の設計・実装を支援しています。 段階的ロールアウトの導入や、A/Bテスト基盤の構築でお困りの際は、お気軽にご相談ください。

お問い合わせはこちら

この内容について相談する他の記事を見る