Feature Flag導入で本番バグ対応が1時間から5分に短縮した話

リリース直後のバグで1時間かかった対応がFeature Flagで5分に。段階的ロールアウト、即座なロールバック、A/Bテストをコードデプロイなしで実現した実装例を公開します。

なぜ急に本番環境でFeature Flagに力を入れたのか

うちのチームで去年の話なんですけど、ある新機能をリリースした直後に、思わぬバグが3%のユーザーに影響を与えちゃったんですよ。幸い深刻ではなかったけど、その時点では「リリース→全ユーザーに即座に展開」という従来のフローしかなくて、対応に1時間以上かかってしまった。その経験から「これはFeature Flagの導入を本気で検討すべき」という結論に至ったんです。

実装してみたら、正直なところ最初は「ログやメトリクスが増えるだけじゃないか」って懐疑的だったチームメンバーもいたんですが、実際に運用してみると劇的に変わりました。本番環境での機能制御、段階的ロールアウト、即座なロールバック—これらが全部コードデプロイなしにできるようになったのは、マジで助かってる。今は「Feature Flagなしでリリースするなんて考えられない」くらいになってますね。

最初の課題:どのFeature Flag管理ツールを選ぶか

Feature Flagツール自体は2026年時点でかなり成熟していて、選択肢は豊富です。うちが検討した主なものを比較してみました。

ツール強み弱み向く場面
Unleash (オープンソース)セルフホスト可能、低コスト、SDK豊富インフラ管理が必要、高度なセグメンテーション機能は弱いエンタープライズで費用削減重視、オンプレミス環境
LaunchDarklyUI/UX優秀、セグメンテーション強力、A/Bテスト統合費用が高い(1ユーザーあたり年間数万円)スタートアップから大規模企業、すぐ本気運用したい場合
Flagsmith中程度の機能とコストのバランス、モダンUILaunchDarkneyより機能は劣るSMB向け、成長中のスタートアップ
StatsigAI駆動の実験、超低レイテンシ、新機能が豊富日本向けサポートが薄い実験を大量にやる企業、超高速レスポンスが必須

うちは「メンバーが20人以下だし、AWS環境で自由に構築できる」という条件から、Unleashのセルフホスト版をECS on Fargateで運用するという判断をしました。月の費用は基本的にはFargate・RDS・S3のコストだけで、大体2万円前後。LaunchDarkneyなら同規模で月10万円以上かかるので、1年で100万円近い差になる。

# docker-compose.yaml で Unleash をローカル検証
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: unleash
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"

  unleash:
    image: unleashorg/unleash:latest
    environment:
      DATABASE_URL: postgres://postgres:secret@postgres:5432/unleash
      UNLEASH_URL: http://localhost:4242
      INIT_FRONTEND_API_AUTH_TOKENS: "secret-token"
    ports:
      - "4242:4242"
    depends_on:
      - postgres

実装当初は「セルフホストだから運用大変じゃないか」って心配もあったんですが、Unleash自体がシンプルな設計で、アップデートも月1回程度、障害もほぼなし。むしろストレスフリーです。

実装パターン:段階的ロールアウトからカナリアリリースまで

Feature Flagの実装って、結局のところ「条件判定ロジック」なんですよね。SDK経由でフラグの状態を取得して、それに基づいて処理を分岐させるだけ。いくつかのパターンを紹介していきましょう。

パターン1:シンプルなOn/Off

まずは最もシンプルなパターンから。

// Next.js + TypeScript の例
import { UnleashClient } from 'unleash-client-node';

const client = new UnleashClient({
  url: 'https://unleash.example.com/api',
  clientKey: process.env.UNLEASH_TOKEN,
  appName: 'my-app',
});

export default async function handler(req, res) {
  // フラグ名 "newCheckoutFlow" が有効か判定
  const isEnabled = client.isEnabled('newCheckoutFlow');

  if (isEnabled) {
    // 新しい実装
    res.json({ checkoutUrl: '/checkout/v2' });
  } else {
    // 従来の実装
    res.json({ checkoutUrl: '/checkout/v1' });
  }
}

パターン2:ユーザーセグメント別のロールアウト

本番環境で一気にリリースするんじゃなくて、段階的に展開したい場合があります。うちの場合、新しい決済フローをリリースする時は「まず内部ユーザー(従業員)にだけ有効にして、問題がなければ段階的に他のユーザーに広げる」という運用をしてました。

// Unleash の Context を使ったセグメンテーション
const context = {
  userId: user.id,
  sessionId: session.id,
  properties: {
    email: user.email,
    accountType: user.accountType, // 'internal', 'vip', 'regular'
    signupDate: user.createdAt,
  },
};

const isEnabled = client.isEnabled('newCheckoutFlow', context);

Unleashの管理画面では、このフラグに対して「accountType === 'internal' のユーザーに100%有効にする」という条件を設定できます。さらに進めて、翌週は「signupDate が特定期間のユーザーに50%有効」→「全ユーザーの30%に有効」→「全員有効」という段階的な展開ができるんです。

**実際にこれをやってみた効果を話すと、**内部ユーザー(50人)で2日間検証→顧客ユーザーの5%(300人)で3日間→20%で1週間→本番展開という流れで進めたんですけど、このフローで本番環境のバグを事前に6個発見できました。全員に一気にリリースしていたら、かなりの顧客に影響が出ていたはず。段階的ロールアウトのおかげで、かなり安心感を持ってリリースできるようになったんですよね。

パターン3:A/Bテストとしてのカナリアリリース

もう一歩進んで、「新機能がユーザー満足度を上げるのか」を検証したい場合もあります。うちのプロダクトでは、新しいUIパターンをリリースする時に、ユーザーを50%/50%で分割して「どちらのUIが離脱率が低いか」を測定しました。

// フロントエンド側の実装例(React)
import { useFeatureFlag } from '@unleash/client-react';

function CheckoutPage() {
  const { isEnabled: useNewUI } = useFeatureFlag('newCheckoutUI');

  // ユーザーのセッションIDをベースに一貫したフラグを取得
  // Unleashが同じセッション内では同じ分岐を返すようにしてくれる

  if (useNewUI) {
    return <NewCheckoutUI />;
  } else {
    return <LegacyCheckoutUI />;
  }
}

// バックエンド側でイベント送信
import { analytics } from './analytics';

function onCheckoutComplete(userId, variantUsed) {
  analytics.track('checkout_complete', {
    userId,
    variantUsed, // 'new' or 'legacy'
    timestamp: new Date(),
  });
}

1週間実験してみた結果がこちらです。

xychart-beta
    title "A/B テスト結果:新UI vs 従来UI(1週間)"
    x-axis [Day1, Day2, Day3, Day4, Day5, Day6, Day7]
    y-axis "離脱率 (%)" 0 --> 25
    line [15, 14, 14, 13, 13, 12, 11] comment "新UI"
    line [20, 20, 20, 19, 19, 19, 18] comment "従来UI"

結果として、新UIの方が離脱率が3%低かったので、「全員に新UI展開するのはユーザー体験向上につながる」という根拠を得られました。なんとなく「新しい方が良さそう」じゃなくて、データに基づいた判断ができるのって、マネジメント層へのレポートでも説得力が全然違うんですよね。経営判断とも一致しやすくなるし、チーム内での合意形成も楽になります。

実装時の落とし穴:キャッシング問題とランタイムパフォーマンス

正直なところ、最初の実装で痛い目を見たのが「フラグ値のキャッシング」です。Unleashクライアントがサーバーからフラグ情報を定期的に同期するんですけど、ネットワーク遅延やサーバーの問題で同期が失敗することがあります。

// 最初のやり方:毎回サーバーに問い合わせ(遅い)
app.get('/api/data', async (req, res) => {
  const isEnabled = await unleashClient.isEnabled('myFlag');
  // 大量リクエストがくると Unleash サーバーへの問い合わせが多発
  res.json({ data: isEnabled ? newData : oldData });
});

これをやると、フラグ判定のレイテンシが 20-50ms 増加して、API全体の応答時間が悪化します。Unleashは自動キャッシュ機能を持ってるんですが、デフォルトは15秒。更新頻度が高い場合は足りません。

改善後:

// 改善:メモリ内キャッシュ + イベント駆動での更新
const flagCache = new Map();

// Unleash クライアントが更新されたら、すぐにメモリキャッシュを更新
client.on('update', (name, flag) => {
  flagCache.set(name, flag);
});

app.get('/api/data', (req, res) => {
  // キャッシュから即座に取得(<1ms)
  const isEnabled = flagCache.get('myFlag') || false;
  res.json({ data: isEnabled ? newData : oldData });
});

// フラグ値がアウトデートしたら、バックグラウンドで同期
client.start();

これでレイテンシを 0.1ms 以下に削減できました。重要な観点は「キャッシュが古くなること」よりも「API応答時間」なので、少し古い値でも構わない。ただしクリティカルなフラグ(例:セキュリティ関連)は無視できない遅延を許容しないので、そういう場合は冗長性を持たせた別の仕組みを用意してます。

本番運用で学んだ6ヶ月分のノウハウ

1. フラグの命名規則を徹底する

うちは当初、フラグ名が「v2Checkout」「newUICheckout」「checkout_v2」みたいにバラバラだったんです。3ヶ月経つと、「このフラグ何に使ってるの?」ってなるんですよね。いまは {機能エリア}_{実装版}_{説明} の形式に統一しました。

正:checkout_v2_payment_gateway
正:user_profile_ai_recommendations
悪:v2
悪:newThing

統一しただけで、新しい人がプロジェクトに入った時のオンボーディングがめっちゃ楽になりました。

2. フラグの「削除計画」を決めてからリリースする

A/Bテストなり段階的ロールアウトが完了したら、フラグは削除すべきです。そうしないとコードベースに死んだコードが溜まっていく。うちはいまリリース時に「このフラグを削除する予定日」を決めて、その日が近づいたら自動で Slack 通知が来る仕組みを作りました。地味だけど、コード品質を維持する上で意外と重要です。

3. ドキュメント化するなら、フラグの定義ファイルを自動生成

// flags.ts (自動生成)
export const FLAGS = {
  CHECKOUT_V2_PAYMENT_GATEWAY: {
    name: 'checkout_v2_payment_gateway',
    description: '新しい決済ゲートウェイに切り替え',
    owner: 'payment-team',
    rolloutStartDate: '2026-01-15',
    rolloutEndDate: '2026-02-15',
    deletionDate: '2026-03-01',
  },
  // ... 他のフラグ
} as const;

// コード内でも IDE が補完できる
const isEnabled = client.isEnabled(FLAGS.CHECKOUT_V2_PAYMENT_GATEWAY.name);

こうしておくと、フラグ名の誤字や、削除済みフラグの参照を事前に検出できます。

4. メトリクスをちゃんと計測しないと判断が遅延する

フラグを有効にした時の「影響」を測定する仕組みが不十分だと、ロールバックすべき時期を見逃します。うちはいまフラグごとに主要メトリクスを定義していて、異常値を検出したら自動でアラートが出る設定にしてます。

// feature-flag-metrics.ts
export const FLAG_METRICS = {
  checkout_v2_payment_gateway: {
    metrics: [
      { name: 'payment_success_rate', threshold: { min: 0.96 } },
      { name: 'payment_latency_p99', threshold: { max: 3000 } },
      { name: 'payment_error_rate', threshold: { max: 0.02 } },
    ],
    evaluationInterval: 300, // 5分ごと
  },
};

これがあると「あ、新しい決済フローでエラー率上がってるな」ってすぐに気づけます。

別の記事との関連性

Feature Flagは インシデント対応 とも深い関係があります。本番環境で問題が発生した時、即座にフラグを Off にしてロールバックできるというのは、従来のホットフィックスデプロイより圧倒的に速い。うちもこれで何度か助かってます。

同様に SLI/SLO設計 の観点からも、Feature Flagは重要です。段階的ロールアウトで SLO 違反が発生しないか確認してから本展開する、という運用ができるんですよね。

まとめ

  • Feature Flag導入で本番環境のリスクを大幅削減:段階的ロールアウトで事前にバグを発見でき、問題があれば即座にロールバック可能
  • セルフホスト版Unleashなら費用も抑える:月2万円の運用コストで、大企業向けツールと同等の機能を実現
  • A/Bテストによるデータ駆動意思決定:「新しい方がいい」という推測ではなく、数値根拠でリリース判断ができる
  • キャッシング設計とメトリクス監視が運用の鍵:レイテンシ問題を避け、フラグごとのメトリクス異常をリアルタイム検出
  • 次のステップ:フラグの自動削除とドキュメント自動生成:コードベースの品質維持と、チーム全体でのベストプラクティス共有

うちのチームではいまFeature Flagが当たり前になってて、「フラグなしでリリースする意思決定」すら稀です。特にマイクロサービスやマルチテナント環境では、顧客ごとに機能の有効/無効を切り替えたいシーンも増えると思うので、早めに導入して運用ノウハウを溜める価値は十分あると感じています。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事