本文へスキップ
webkida wiki
戻る

【Feature Flag】デプロイとリリースを分ける

本番にコードを載せることと、ユーザーに機能を見せることを分ける。フラグの値をどこに置き、リクエストのときにどこを見るかまで整理する

コードを本番に載せることと、ユーザーにその機能を見せることは、別のタイミングにできます。Feature Flag は、その間に置くスイッチです。

ブランチの話で、main に戻したあとも見せない、という使い方は 【Git】Git FlowとGitHub Flowの違い にあります。ここでは、フラグをどこに置き、リクエストのときにどこを見るかを整理します。

用語

Feature Flag

機能ごとに持つ ON / OFF のスイッチです。コードが本番にあっても、フラグが OFF なら、その機能はユーザーに出ません。

デプロイ

そのコードが本番環境で動いていることです。フラグの状態とは別です。

リリース

ユーザーがその機能を使えるようにすることです。フラグを ON にしたときが、その機能のリリースです。

Feature Flagとは

Feature Flag は、デプロイとリリースを分けるための仕組みです。

新機能を main に戻して本番まで載せても、フラグを OFF にしておけば、ユーザーには出ません。見せると決めたとき、ブランチを切り直さず、フラグを ON にします。

sequenceDiagram
    participant Dev as 開発者
    participant Prod as 本番
    participant User as ユーザー

    Dev->>Prod: ① main へ戻してデプロイ
    Note over Prod: ② コードは本番にある<br/>Feature Flag は OFF
    Prod-->>User: ③ ユーザーには出ない
    Dev->>Prod: ④ Feature Flag を ON
    Prod-->>User: ⑤ ユーザーに公開

図: デプロイしたあともフラグは OFF のままにできる。ON にしたときが、その機能のリリース。

Deploy  = コードを本番環境に置く
Release = 機能をユーザーが使えるようにする

GitHub Flow や Trunk-Based Development では、作業ブランチを長く開いたままにせず、小さい単位で main に戻します。まだ見せない機能は、フラグで隠します。

フラグは必須ではありません。短く終わる機能は、作業ブランチのうえで完成させてから main に戻すだけで足ります。main へのマージを契機にデプロイする構成なら、戻した時点でデプロイされ、同時にリリースになります。

環境変数との違い

Feature Flag は仕組みの名前です。環境変数は、その値の置き方の一つです。

if (process.env.NEW_PAYMENT_ENABLED === "true") {
  showNewPayment();
}

NEW_PAYMENT_ENABLED=false としておけば、この分岐はフラグとして働きます。ここで分かれるのは、元の値をどこで管理するかと、アプリケーションがそれをどう参照するかです。

元の値は、環境変数、DB、設定ストア、Feature Flag の専用サービスに置きます。SDK や Sidecar は、その置き場所ではありません。専用サービスから状態を受け取って、手元に保持する側です。

sequenceDiagram
    participant App as アプリケーション
    participant Env as 環境変数
    participant DB as DB / 設定ストア
    participant Svc as 専用サービス
    participant SDK as SDK
    participant Agent as Agent

    rect rgba(120, 140, 180, 0.1)
        Note over App,Svc: 元の値を直接見る
        App->>Env: ① 環境変数を見る
        App->>DB: ② DB を見る
        App->>Svc: ③ 専用サービスを見る
    end

    rect rgba(120, 160, 130, 0.1)
        Note over App,Agent: 受け取って持つ側
        Svc-->>SDK: ④ SDK が状態を保持
        App->>SDK: ⑤ アプリは SDK を見る
        Svc-->>Agent: ⑥ Agent が状態を保持
        App->>Agent: ⑦ アプリは localhost を見る
    end

図: 左が元の値の管理場所。右がアプリの参照。SDK と Sidecar は、サービスから状態を受け取って持つ側。

環境変数のまま使うと、動いているプロセスの値はあとから書き換わりません。変えるには再起動か、ECS なら Task の再デプロイです。社員だけ先に出す、5% だけ出す、といった分け方も、環境変数だけではやりにくいです。

そこまでしたいとき、フラグ用のサービスを置きます。置いたあとで、次の問いが出ます。

リクエストのたびに聞きに行くのか

「ユーザーのリクエストごとに、フラグのサービスへ聞きに行くのか」

まず思いつく形は、それです。

sequenceDiagram
    actor U as ユーザー
    participant App as アプリケーション
    participant FF as Feature Flag のサービス

    U->>App: ① リクエスト
    App->>FF: ② フラグを問い合わせる
    FF-->>App: ③ フラグを返す
    App-->>U: ④ その値で処理する

図: リクエストのたびに Remote へ聞く。実装は単純で、応答の途中にネットワークが挟まる。

実装は単純です。ここで気になるのが、遅くならないか、です。

毎回、ユーザーの応答の途中にネットワークが入ります。フラグのサービスが遅ければ、画面も遅れます。そのサービスが止まれば、フラグを見るリクエストも止まります。呼び出しは、リクエストの数だけ増えます。

毎回 Remote へ聞くのが、唯一の形ではありません。この心配を、リクエストの経路から外すために、よくある置き方が分かれます。

毎回 Remote へ聞く

心配の元になる形です。フラグの参照が少なく、その遅さを許容できるなら、このままで足ります。許容できないなら、聞きに行くタイミングをリクエストの外へ出します。

環境変数に置く

サービスへ聞きに行かない、いちばん端の形です。Task に NEW_PAYMENT=true のような値を渡し、アプリケーションは process.env を見るだけです。ネットワークが挟まらないので、参照は速いです。

その代わり、すでに動いているプロセスの値は書き換わりません。false から true にするには、Task の再起動か再デプロイです。何度も切り替えるフラグには向きません。リリースのたびに一度だけ切り替える ON / OFF なら、これで足りることもあります。

SDK に控えさせる

リクエストの途中で Remote へ聞かないなら、どこかに値を控えておく必要があります。SDK 方式は、アプリケーションのプロセスの中に SDK を入れ、メモリに控えます。ユーザーのリクエストが見るのは、そのメモリです。

sequenceDiagram
    actor U as ユーザー
    participant App as アプリケーション
    participant SDK as SDK
    participant Mem as メモリ上のキャッシュ

    U->>App: ① リクエスト
    App->>SDK: ② フラグを聞く
    SDK->>Mem: ③ メモリだけを見る
    Mem-->>SDK: ④ true / false
    SDK-->>App: ⑤ 結果を返す

図: リクエストが見るのはメモリだけ。この図には、Remote への矢印がない。

Remote とのやりとりは、リクエストの途中には入れません。キャッシュをいつ新しくするかは、別のタイミングです。製品によって、次のどれかになります。

Polling は、起動時にフラグを取ってメモリに置くやり方です。一定時間ごとに SDK が Remote へ聞き、キャッシュを更新します。その問い合わせは、ユーザーの応答を待たせません。

Streaming は、フラグが変わったときにサービスが SDK へ通知するやり方です。SDK はその通知でメモリを更新します。リクエストが通知を待ちません。

Lazy Load は、参照したときにキャッシュが古ければ、その場で Remote から取るやり方です。古いときだけ、リクエストの途中にネットワークが戻ってきます。

Sidecar に分けて持たせる

キャッシュができてからのリクエストは、Remote を見ません。違うのは、控えと更新をアプリケーションのプロセスから出すことです。ECS なら、同じ Task の中に Agent を置きます。AWS では AWS AppConfig Agent がこの形です。

初回は例外です。AppConfig Agent は、最初の取得でキャッシュを作ります。その応答は、キャッシュがあるあとのリクエストより遅くなり得ます。

sequenceDiagram
    participant App as アプリケーション
    participant Agent as Sidecar / Agent

    App->>Agent: ① localhost でフラグを聞く
    Agent->>Agent: ② Agent のキャッシュを見る
    Agent-->>App: ③ true / false

図: アプリケーションは localhost の Agent を見る。Remote への更新は、この図の外で Agent が行う。

この構成では、取得は Agent に任せ、アプリケーションは localhost を参照します。Agent が最新の値を取ってキャッシュし、アプリケーションはそのキャッシュを見ます。アプリが AWS の API に届かない、という決まりではありません。

SDK と Sidecar のどちらにするか

どちらも、遅くならないかという心配に対して、キャッシュができてからはリクエストの途中で Remote を見ません。残る違いは、そのキャッシュを誰が新しくするかです。

SDK では、アプリケーションのプロセスが新しくします。Sidecar では、別のプロセスが新しくします。アプリケーションは localhost を見るだけです。

sequenceDiagram
    participant App as アプリケーション
    participant Mem as プロセス内のメモリ
    participant Agent as 別プロセスのキャッシュ

    rect rgba(120, 140, 180, 0.1)
        Note over App,Mem: SDK
        App->>Mem: ① 同じプロセスのメモリを見る
    end

    rect rgba(120, 160, 130, 0.1)
        Note over App,Agent: Sidecar
        App->>Agent: ② 別プロセスのキャッシュを見る
    end

図: リクエストが見る場所は、どちらも手元。更新の担当が、同じプロセスか、別プロセスか。

sequenceDiagram
    actor U as あなた
    participant Q as 置き方

    U->>Q: ① リクエストの途中で<br/>Remote へ聞く遅さを許容できるか
    alt 許容できる
        Q-->>U: 毎回 Remote へ聞く
    else 許容できない
        U->>Q: ② 出したあとも、再デプロイなしで切り替えたいか
        alt 切り替えは少ない
            Q-->>U: 環境変数
        else 切り替えたい
            U->>Q: ③ キャッシュの更新を<br/>アプリのプロセスが持つか
            alt 持つ
                Q-->>U: SDK とメモリ
            else 分けたい
                Q-->>U: Sidecar / Agent
            end
        end
    end

図: まず遅さを許容できるか。できないなら、切り替えの頻度と、更新を誰が持つかで置き方が分かれる。

遅さを許容できるなら、毎回 Remote へ聞く形のままです。許容できず、切り替えも少ないなら、環境変数です。出したあとも割合や対象を変えたいなら、SDK か Sidecar です。更新をアプリケーションに持たせてよいなら SDK、その仕事をプロセスの外へ出したいなら Sidecar です。

まとめ

Feature Flag の中心は、true / false を持つことではありません。デプロイとリリースを分けることです。

フラグ用のサービスを置いたとき、まず気になるのは、リクエストのたびに聞きに行くと遅くならないか、です。毎回聞く形は単純ですが、応答の途中にネットワークが入ります。その心配を外す置き方が、環境変数と、SDK のメモリと、Sidecar のキャッシュです。

決めるのは、値をどこに保存するかだけではありません。リクエストの途中で Remote を見るのか、キャッシュを誰が新しくするのか、までです。


この記事をシェア:

前の記事
【HTTP】Cookieの属性と送信条件
次の記事
【Git】Git FlowとGitHub Flowの違い