開発スピード2倍計画のための布石、Feature Flagの導入と運用

2025/01/27に公開

こんにちは!
ourly株式会社 執行役員CTO(@tigers_loveng)の相澤です。

我々ourlyプロダクトチームでは、今Qのテーマ(KGI)として「開発スピード2倍」を掲げています。
そのための評価指標(KPI)として以下3つを追うことにしました。

KPI 具体数値
PR作成数2倍! FE: 4件/day、BE: 2件/day
PRサイクルタイムを維持! 平均15h
(プレ)リリース※頻度2倍! 1回/2w

この目標を決めた背景はまたそのうちどこかで話せればと思いますが、これらの目標を達成するために必要不可欠な要素としてFeature Flagがあります。

去年の2Q(24/07)から調査を始め、3Qから本番環境での運用を開始しております。まだ開始から1,2ヶ月しか経っていませんが、僕個人の感想としては、デプロイのハードルがかなり下がって開発者体験が良化しています。

Feature Flagとは何か、どんなメリットがあるかは先駆者たちがまとめてくれているのでそちらをご参照ください。

https://tech.findy.co.jp/entry/2024/11/11/070000

https://zenn.dev/ascend/articles/feature-flag

Feature Flag導入の意思決定

元々Feature Flagの存在は知っていましたが、一昨年、Four keysを中心に開発生産性向上に取り組むまでは、あまり必要性を感じることはありませんでした。

というのも、初期リリース後からずっと、developブランチからプロジェクト毎にepicブランチを切り、epicブランチからfeatureブランチを切って開発を行うというスタイルをとっていました。
そしてリリースに必要な全てのfeatureが揃ってから本番環境にデプロイするという流れです。

epicブランチでの開発は、すぐには本番環境にデプロイされないため安心感がある、マイグレーションファイルの修正がやりやすいなどのメリットがある反面、どうしても価値提供スピードが上がりにくいというデメリットもありました。

特に開発生産性向上に取り組み始めてからは、PRのリードタイムは短縮されているのに価値提供スピードが上がっていないことにもやもやし、デメリットの部分を感じることが多くなりました。

そして今期の方針として 「プログラムを積み上げていく開発チーム」から「価値を積み上げていくプロダクトチーム」へと変わる決意 をし、そのための施策としてFeature Flagを導入する意思決定を行いました。

今期の目標決めに関しては以下記事で詳細にまとめていますのでよければご覧ください。

https://qiita.com/KosukeAizawa/items/7c470b642acbc6b82ad7

Feature Flagの導入

意思決定 -> 調査

導入にあたってまずはどのような方法で実現できるのかを調査しました。
調査の際には以下の記事がとても参考になったので、導入を検討している方はぜひ読んでみてください。

https://codezine.jp/article/detail/14442

他にもいくつかFeature Flagを導入して運用している企業のテックブログを読むなどして情報収集してConfuluenceにまとめたメモが以下です。

実現方法として大きく分けると5つあり、それぞれで運用に向いているチーム規模や実現できる幅、導入コストなどが変わってきます。

例えば、一番運用が簡単で導入コストも低い環境変数による切り替えだと、本番環境/ステージング環境/開発環境などの環境ごとのオンオフはできますが、本番環境の特定のテナントやユーザーに対してのオンオフは切り替えられません。

弊社で実現したいこととチーム規模、かけられるコストを考えた時に、オンオフの管理をDBに寄せて行うことがベストだと判断しました。

特に、導入コストの部分でいうと、すでに似たような仕組み(契約プランにより有効化する機能を分ける仕組み)で出しわけをしているところがあったので、実現方法がある程度見えており、不確実性が低かったというのも決め手の一つです。

調査 -> 設計

DBでのFeature Flag管理を決めた後は、実際に開発する時のことを想像しながら以下の観点で設計を行いました。

  • 新規のFeature Flagが追加しやすいか
  • 役目を終えたFeature Flagが削除しやすいか
  • オンオフの切り替えが簡単にできるようになっているか

この中でも、特に2点目の削除しやすいかどうかはかなり重要な要素であると考えました。例えば、一つのメソッドの中で複数回判定ロジックが登場するようなことがあると、影響範囲が広がりますし、抜け漏れが発生しやすくなります。

def method
  if tenant.feature_enabled?(:feature1) & tenant.feature_enabled?(:feature2)
    # 処理
  elsif tenant.feature_enabled?(:feature3)
    # 処理
  else
    # 処理
  end

  # 処理

  return hoge if tenant.feature_enabled?(:feature2)

  # 処理

  if tenant.feature_enabled?(:feature1) & tenant.feature_enabled?(:feature3)
    # 処理
  else
    # 処理
  end

  tenant.feature_enabled?(:feature1) ? fuga : piyo
end

ちょっと極端な例ではありますが、複数人で複数Feature Flagの運用をしていればあり得なくはないと思います。

そもそもこれだとテストするだけでも大変ですよね...
生産性を上げようとした結果、生産性が下がってしまい逆効果にもなり得ます。こうなってしまっては本末転倒なので、だからこそ削除時のことをよく考えることが重要なのです。

サンプルコードを作り、メンバーと議論した結果、以下のような設計となりました。
なお、あくまでイメージを掴みやすくするためのサンプルコードなので、実際のプロダクトコードとは少し異なります。

FE

export const TestPage = () => (
  <FeatureFlag
    oldComponent={<Hoge />}
    newComponent={<Fuga />}
    featureName="fugaFlag"
  />
)
type Props = {
  oldComponent?: ReactElement
  newComponent: ReactElement
  featureName: string
}

export const FeatureFlag: FC<Props> = (props) => {
  const { oldComponent, newComponent, featureName } = props
  const featureFlags = useSelector(someSelector)
  const enableFeature = featureFlags.includes(featureName)

  if (enableFeature) return newComponent
  return oldComponent ?? null
}

Feature Flagを出し分けるコンポーネントを一つ用意し、oldとnewの出しわけパターン、Feature Flagのキー名を渡すようにしています。

もしfugaFlagを消すことになった場合は、シンプルにFeatureFlagコンポーネントとfugaFlagを使用している箇所を抽出し、Fugaコンポーネントに置き換えるだけとなります。
機械的にできるため、削除もしやすいですし、コンポーネント呼び出し箇所を追うことで抜け漏れも発生しづらくなります。

BE

まず、テーブル構造としてはシンプルにtenantsテーブルとfeature_flagsテーブルを用意し、中間テーブルで2つのテーブルを関連付ける形にしております。

feature_flagsテーブルに全公開フラグを持たせることで、

  • プレリリースからリリースへの移行がスムーズになる
  • 万が一クリティカルな不具合があった場合に、すぐに機能をオフにできる

といった利点があります。

テナントごとのFeature Flagの有効/無効の判定は、既存の権限管理のために使っていたcancancanというgemで行います。

Tenantモデルに、渡されたFeature Flagが有効かどうかを判定するインスタンスメソッドを用意します。
直接テナントに紐づいている有効なFeature Flagのリストを取得するactive_feature_flagsというアソシエーションと、全公開されているFeature Flagを取得するscopeを別で定義しています。

# @return [Set<Symbol>]
def enabled_feature_flags
  @enabled_feature_flags ||= Set.new.tap do |set|
    [active_feature_flags, FeatureFlag.all_opened].each do |feature_flags|
      set.merge(feature_flags.pluck(:name).map(&:to_sym))
    end
  end
end

# @param [String, Symbol] name
# @return [Boolean]
def feature_flag_enabled?(name)
  enabled_feature_flags.include?(name.to_sym)
end

テナントごとの権限判定は、主にアクション単位で設定する形としました。処理の中で行おうとすると、前述した問題が起きうるため、その場合はいっそのことアクションごと分けてしまおうという発想です。

module Abilities
  class FeatureFlag
    include CanCan::Ability

    # @param [User] user
    def initialize(user)
      return if user.nil?

      if user.tenant.feature_flag_enabled?(:hoge)
        can(:index, Api::HogeController)
      end
    end
  end
end

こうすることで、リリース済みとなったFeature Flagを削除する際には上記の判定処理をブロックごと消すだけでOKです。

運用時の注意点

Feature Flagを運用するにあたって、負債化するリスクを下げるためにルールを決めてチーム全体に周知を行いました。
具体的には以下2点です。

  1. Feature Flag同士の依存関係を作らない
    a. Feature Flag同士で依存関係を作ってしまうと、運用負荷(テスト工数、動作確認工数、コードの可読性&保守性DOWN…etc)が格段に上がるため
    b. もし依存関係が出てきそうな時は、元のFeature Flagを消す動きを優先的にとること
  2. Feature Flagが全テナント公開状態になった場合、1ヶ月を目処に削除するタスク(チケット起票)を必ず入れる
    a. Feature Flagが乱立するとどの状態で何が表示されるのか(できるのか)が分からなくなり、運用負荷が上がるため
    b. 一旦制限は特にしていないが、もし管理工数が問題視され始めたら同時運用数を強制的に制限するような形にすることも視野に入れておく

今回は、弊社でのFeature Flagの導入検討から実際の運用開始までのあれこれを紹介させていただきましたがいかがでしたでしょうか?

これから導入を検討している方にとって参考になる情報となっていれば幸いです。

ourly tech blog

Discussion