コードをデプロイしても機能をリリースしたことにはならない

チームが検索アルゴリズムの大規模リファクタリングを終えたとしよう。コードはテストされ、レビューされ、本番環境にデプロイされた。しかし、まだ誰も使っていない。新しいアルゴリズムはサーバ上でコンパイルされ、準備は整っているが、すべてのユーザは依然として旧アルゴリズムの結果を見ている。これは意図的なものだ。

このシナリオは、デプロイした変更が即座にユーザに影響する環境に慣れていると奇妙に聞こえるかもしれない。しかし実際には、多くのチームが同一視している「コードのデプロイ」と「機能のリリース」という二つを分離する強力なパターンである。

デプロイだけでは解決できない問題

カナリアリリースや段階的ロールアウトは、一つの問題をうまく解決する。それは、新しいバージョンのアプリケーションにどの程度のトラフィックを流すかを制御することだ。ユーザの5%を新バージョンに送り、エラーを監視し、その後100%まで段階的に増やす。これはバージョン全体を徐々にロールアウトしたい場合に有効だ。

しかし、異なるコードバージョンをデプロイせずに、特定のユーザに対してのみ特定の機能を有効にしたい場合はどうだろうか。内部テスターだけに新しい検索結果を表示し、他のユーザは旧アルゴリズムのままにしたい。特定の地域のユーザにのみ機能を有効にしたい。モバイルアプリのユーザの10%で新しいチェックアウトフローをテストしたい。

カナリアリリースや段階的ロールアウトではこれらを解決できない。これらはバージョンレベルで動作し、機能レベルではない。リクエストごと、ユーザごと、セッションごとに判断を行う、コード内部に存在する何かが必要だ。

フィーチャーフラグ:コード内部の制御レイヤ

フィーチャーフラグは、特定のユーザに対して機能を有効にするかどうかを判断する、コード内の条件分岐である。重要なのは、その条件が実行中のコードのバージョンに基づいていないことだ。再デプロイなしでいつでも変更可能な設定に基づいている。

実際の例を見てみよう。チームが新しい検索アルゴリズムを完成させた。コードは最新バージョンの一部として本番環境にデプロイされている。しかし、検索関数の中には次のようなチェックがある:

以下は同じパターンのJavaScriptの例である:

const featureFlags = require('./feature-flags');

function search(query, user) {
  if (featureFlags.isEnabled('new-search', user)) {
    return newSearchAlgorithm(query);
  } else {
    return oldSearchAlgorithm(query);
  }
}

// フラグはデフォルトでオフ。全ユーザが旧パスを通過する。
// 準備ができたら、ダッシュボードからテスター向けにフラグをオンにする。
if feature_flag.is_active("new_search_algorithm", user):
    return new_search_algorithm(query)
else:
    return old_search_algorithm(query)

フラグはデフォルトでオフになっている。新しいコードが同じサーバ上に存在していても、すべてのユーザは旧アルゴリズムを使い続ける。準備ができたら、内部テスター向けにフラグをオンにする。彼らは新しい結果を見始める。その挙動を監視する。問題があれば、フラグを再びオフにする。ロールバックも再デプロイも不要だ。数秒で反映される設定変更だけで済む。

なぜデプロイとリリースを分離するのか

第一のメリットは心理的かつ実践的である。デプロイが高リスクなイベントではなくなる。すべてのデプロイですべての変更が即座に有効になる場合、作業をまとめ、徹底的にテストし、リリースウィンドウ中は息を詰めて見守ることになる。デプロイとリリースが分離されていれば、新しいコードは明示的に有効にするまで休止状態にあると分かっているため、毎日、あるいは1日に複数回デプロイできる。

第二のメリットは細かい制御が可能なことだ。フィーチャーフラグは全員に対してオン/オフという単純なものではない。ターゲティングルールを定義できる:

  • 特定のIDを持つユーザに対して有効
  • 特定の地域のユーザに対して有効
  • 内部テスターやベータユーザに対して有効
  • 全ユーザの一定割合に対して有効

これらのルールは組み合わせることもできる。まずユーザの5%で開始し、1日観察してから25%、50%、そして100%に増やす。すべての変更はダッシュボードやAPI呼び出しを通じて行われる。コード変更もデプロイも不要だ。

第三のメリットはキルスイッチ(緊急停止機能)だ。機能を有効化した後に問題が発生した場合、一箇所からその機能を無効化できる。ロールバックパイプラインが完了するのを待つよりもはるかに高速だ。数秒で問題のある機能はオフになり、ユーザは旧動作にフォールバックする。本番インシデントでは、その数秒が重要だ。

隠れたコスト:フラグ負債

フィーチャーフラグはコードベースに複雑さをもたらす。すべてのフラグは保守が必要な条件分岐を導入する。チームがフラグを作成して削除しない場合、コードは誰も覚えていないデッドコンディションで埋め尽くされる。

よくあるシナリオ:新しいチェックアウトフロー用にフィーチャーフラグが作成される。フローは全ユーザへのロールアウトに成功する。旧チェックアウトコードは、常にオフのフラグでガードされたまま残される。数ヶ月後、新しい開発者がそのフラグを見て、まだ有効かどうか疑問に思う。誰も知らない。削除するのはリスクが伴うように感じられるため、フラグは残り続ける。

これがフラグ負債であり、フィーチャーフラグの主な運用コストである。フラグにはライフサイクルが必要だ:作成、有効化、監視、クリーンアップ。機能が全ユーザに完全にロールアウトされたら、旧コードパスとフラグは削除すべきだ。フラグはその役割を果たした。残しておくことは認知負荷を増やし、バグの表面積を広げるだけである。

フラグ管理アプローチの選択

小規模チームやシンプルなユースケースでは、フィーチャーフラグはアプリケーションが起動時に読み込む環境変数や設定ファイルとして始められる。これは機能するが、フラグを変更するには再起動または設定の再読み込みが必要という制限がある。

大規模チームやより複雑なターゲティングルールが必要な場合は、LaunchDarkly、Split、Flagrなどの専用フラグ管理プラットフォームが、リアルタイムのフラグ評価、ユーザターゲティング、監査ログを提供する。これらのプラットフォームを使えば、ダッシュボードからフラグを変更し、すべての実行中インスタンスに即座に反映させることができる。

適切な選択は規模に依存する。シンプルに始め、必要なときにのみ複雑さを追加する。

フィーチャーフラグ使用の実践的チェックリスト

  • 各フラグには明確な所有者と目的があり、可視化された場所に文書化されている
  • フラグには計画された削除日またはトリガー条件がある(例:「ロールアウトが100%に達したら削除」)
  • フラグが不要になったら、旧コードパスは削除される
  • フラグの変更は誰がいつ何を変更したかが記録される
  • 重要なフラグには、誰がどのバリアントを見ているかを示す監視ダッシュボードがある
  • チームは古いフラグを定期的にクリーンアップする習慣がある

まとめ

フィーチャーフラグは、デプロイパイプラインとは独立して動作する制御レイヤを提供する。カナリアリリースと段階的ロールアウトは、新しいバージョンにどの程度のトラフィックを流すかを制御する。フィーチャーフラグは、同じバージョン内でどのユーザがどの機能を見るかを制御する。これらを組み合わせることで、変更がユーザに届く方法をきめ細かく制御できる。ただし、作成したすべてのフラグは、その役割を終えたらクリーンアップすべき技術的負債であることを忘れてはならない。