Kill Switch:ロールバックせずに壊れた機能を停止する

新機能をユーザーの10%に公開した直後、5分も経たないうちにエラー報告が殺到し始めた。その機能は一部のユーザーでページ読み込みを破壊し、別のユーザーではデータを破損させている。機能が有効なままの1秒ごとに、影響を受けるユーザーが増えていく。直感的にはデプロイ全体をロールバックしたくなるが、それには時間がかかる。パイプラインの実行、イメージのプッシュ、サーバーの再起動が必要だ。その間もユーザーは壊れたコードにアクセスし続ける。

ここでKill Switchが非常ブレーキとして機能する。

Kill Switchの本質

Kill Switchとは、アプリケーション全体を以前のバージョンに戻すことなく、問題のある機能だけを無効化できる仕組みだ。フィーチャーフラグを使っている場合、Kill Switchは単にフラグの値をtrueからfalseに変更するだけである。フラグが切り替わった瞬間、アプリケーションは古いコードパスを実行し始める。新機能を表示していたユーザーは、以前のインターフェースやフローに戻る。再デプロイもロールバックも不要。パイプラインの完了を待つ必要もない。

Kill Switchとロールバックの違いは本質的だ。ロールバックはアプリケーション全体を以前のバージョンに戻す。つまり、最新リリースで出荷したすべての変更が元に戻る。これには他の問題のバグ修正や、正常に動作していた小さな改善も含まれる。またロールバックには時間がかかる。パイプラインの実行、コンテナイメージの再ビルドとプッシュ、サーバーの再起動が必要だ。一方Kill Switchは、1つの機能だけを無効化する。アプリケーションの他の部分は最新バージョンのまま動作し続ける。

以下のタイムラインは、Kill Switchがフルロールバックと比較してどれだけ迅速にユーザー影響を止められるかを示している。

以下は、JavaScriptでKill Switchフラグが機能をラップする最小限の例である。

const featureFlags = {
  isEnabled(flagName) {
    // 本番環境ではリモート設定サービスから読み込む
    return config[flagName] === true;
  }
};

function handleCheckout(userCart) {
  if (featureFlags.isEnabled('new-checkout')) {
    // バグの可能性がある新しいチェックアウトフロー
    return newCheckoutFlow(userCart);
  } else {
    // 安定した古いチェックアウトフロー
    return oldCheckoutFlow(userCart);
  }
}

フラグがfalseに切り替わると、アプリケーションは再デプロイなしで即座に古いコードパスにフォールバックする。

flowchart TD subgraph Kill_Switch_Path A1[機能が壊れる] --> B1[フラグを切り替え] --> C1[古いコードが即座に実行] --> D1[ユーザー影響なし] end subgraph Rollback_Path A2[機能が壊れる] --> B2[パイプライン再ビルド] --> C2[再デプロイ] --> D2[サーバー再起動] --> E2[ユーザーが数分間影響を受ける] end A1 -->|時間節約| D1 A2 -->|時間損失| E2

Kill Switchが効果的な場面

Kill Switchは、新しくリリースされたばかりでまだ安定性が確認されていない機能に最も有効だ。例えば、配送料計算にバグがある新しいチェックアウトフローを考えてみよう。Kill Switchが設定されていれば、その新しいチェックアウト機能を即座に無効化できる。ユーザーは古いチェックアウトページに戻る。チームは急ぐことなくバグを修正できる。なぜなら、ユーザーは問題の影響を受けていないからだ。

このパターンは以下のようなケースで効果を発揮する。

  • 実際のユーザートラフィックで壊れる可能性のある新しいUIコンポーネント
  • コアビジネスロジックを変更する実験的な機能
  • ステージングとは異なる動作をするサードパーティ統合
  • まずは少数のユーザーで検証したいリスクの高い変更

重要なのは、Kill Switchが問題のあるコードパスをきれいに分離することだ。フラグがオフの場合、アプリケーションは新機能が導入される前とまったく同じように動作する必要がある。

Kill Switchが不向きな場面

Kill Switchは万能な解決策ではない。問題が新機能自体ではなく、インフラストラクチャの変更やデータベースマイグレーションにある場合、フラグを切り替えても効果はない。例えば、新しいデータベースクエリが本番データベースに過負荷をかけている場合、フィーチャーフラグを無効にしても十分ではない。クエリはすでに実行されているからだ。ダメージは発生している。このような場合、ロールバックかインフラへの直接修正が必要になる。

Kill Switchはコード内での注意深い設計も必要とする。Kill Switchとして機能するフラグは、新しいコードと古いコードをきれいに分離できなければならない。新しい機能がすでにデータベース内のデータを変更している場合、フラグをオフにしてもデータが以前の状態に自動的に復元されるわけではない。チームはKill Switchに依存する前に、これらの副作用を考慮する必要がある。

新しいデータベーステーブルに書き込む機能を考えてみよう。Kill Switchを切り替えると、アプリケーションはそのテーブルへの書き込みを停止するが、すでに書き込まれたデータは残る。古いコードパスがそのテーブルを読み取らない場合、古いデータはすぐに問題を引き起こさないかもしれない。しかし、古いコードパスが異なる形式や場所のデータを期待している場合、後で解消が難しい不整合が発生する可能性がある。

Kill Switchとサーキットブレーカーの併用

一部のチームはKill Switchとサーキットブレーカーを組み合わせて使用している。サーキットブレーカーは、エラー率が定義されたしきい値を超えると、自動的に機能を無効化する。例えば、エラー率が1分以内に5%を超えた場合、サーキットブレーカーは人間の介入なしに機能をオフにする。

この組み合わせは、夜間やチームがオンコールでない時間帯に実行される機能に特に有効だ。サーキットブレーカーは自動化されたセーフティネットとして機能し、Kill Switchは自動システムよりも迅速に行動する必要がある場合に手動オーバーライドを提供する。

サーキットブレーカーパターンはさらに別のレイヤーを追加する。根本的な問題が解決されたことを検出し、徐々に機能へのトラフィックを再導入することもできる。これにより、単純なKill Switchよりも洗練されているが、実装とテストはより複雑になる。

Kill Switch発動後の対応

Kill Switchを切り替えることは緊急対応であり、恒久的な解決策ではない。機能が無効化されたら、チームは根本原因を特定する必要がある。Killされた機能は放棄されるわけではない。バグを修正し、修正をテストし、その後フラグを再度有効にする。

このフォローアップを怠ると、フラグはコードベースに無期限に残り続ける。デッドフラグは技術的負債となる。コードを乱雑にし、将来の開発者を混乱させ、数ヶ月後に誰かが誤って壊れた機能を有効化するリスクを高める。

Kill Switchの実践的チェックリスト

本番環境でKill Switchに依存する前に、以下のチェックリストを確認すること。

  • フラグは副作用なく新しいコードと古いコードをきれいに分離できるか?
  • 機能を無効にしてもデータは一貫した状態を保つか?
  • フラグの切り替えは、デプロイを必要とせずにオンコールチームがアクセス可能か?
  • ステージング環境でKill Switchの動作をテストしたか?
  • チームは誰がKill Switchを切り替える権限を持つか把握しているか?
  • Kill Switch発動後のプロセスが文書化されているか?

具体的な要点

Kill Switchは、アプリケーション全体をロールバックすることなく、1つの機能を数秒で無効化する能力を提供する。ロールバックや適切なテストの代替ではないが、機能を段階的にリリースするすべてのチームにとって重要な安全機構である。フィーチャーフラグがKill Switchとして機能するように設計し、実際に動作することをテストする。そしてKill Switchを切り替えたときは、それを修正サイクルの開始として扱い、会話の終わりとして扱ってはならない。