Feature FlagのSaaSを使うか、OpenFeatureベースでセルフホスティングするか


※この記事は自分が所属する組織で書いた以下の記事のコピーです。投稿した記事は個人の著作物として自ブログにコピーして良いルールとしています。

https://tech-blog.mitsucari.com/entry/2025/02/06/094201


こんにちは、ミツカリCTOの塚本こと、つかびー(@tsukaby0) です。

Feature Flagという仕組みおよびテクニックがあるのですが、弊社ではこれを用いて開発を行っています。

この記事では

  • Feature Flag自体の説明
  • Feature FlagのSaaSであるLaunchDarklyの説明
  • Feature Flagのその他の実現パターンであるOpenFeatureの説明

これらについて扱います。

なお、現状はまだ調査・検討をしたというだけで旧来通りのナイーブな実装(後述)で運用しています。現状ではまだこの部分に特にコストを割くべきではないという判断をしています。個人的には面白そうかつ興味のある部分ではあるので、いつかはもっと改良してみたいと思っています。

Feature Flagとは

Feature Flagについての解説は既に様々な記事があるため、以下を御覧ください。知っている方は読み飛ばしてください。

https://ja.wikipedia.org/wiki/フィーチャートグル

Feature Toggleとも呼ばれています。

以下のアセンド株式会社 丹羽さんの記事では幅広く一通り解説されています。

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

以下のカックさんの記事ではマーチンファウラー氏の記事を例にFlagの種類について解説されています。

https://kakakakakku.hatenablog.com/entry/2022/02/01/102104

以下のourly株式会社 相澤さんの記事ではFeatureFlagコンポーネント化やCanCanCanを組み合わせた実装例が参考になります。

https://zenn.dev/ourly_tech_blog/articles/25ea6f63968a93

ミツカリにおけるFeature Flag

ミツカリでは前述の記事の4種類のFeature Flagのうち、基本的にRelease Toggleのみ活用しています。

ミツカリではGitHub Flowというブランチ戦略を採用しています。詳細は割愛しますが、Git Flowだと顧客への価値提供遅延やconflict解消などのデメリットが大きいため、GitHub Flowとしています。基本的にmasterブランチは常にリリース可能な状態に保つことをルールとしています。

大規模な開発の場合は、Feature Flagを用意して、開発中の機能が本番環境で動かないようにしています。段階的にPRを重ねていき、テンポよく開発できることを意識しています。

Experiment Toggles(実験トグル)とOps Toggles(運用トグル)は特に必要を感じていないため、今まで仕組みを作ったり議論に上がったことはありません。ただし、DBの設定値によってテナントごとにアプリケーションの挙動が変わることはあるので、そういう意味ではOps Togglesは使っているとも言えます。

Permission Toggles(許可トグル)は多少存在します。BtoB SaaSを開発・運営していると個社対応を行うというシーンに直面することがあります。例えば事業や会社がまだ若い状態で大口の顧客が自社向けのカスタマイズをしてほしいという要求をして来た場合、売上や資金繰りを意識して開発するケースはあると思います。ミツカリでは環境変数に挙動をカスタマイズする設定を入れていますが、Ops Toggle(DB)と統一できていませんし、Release Toggle(コード、const)とも統一できていないという問題を持っています。このあたりは今解決すべき課題ではないと思っていますが、将来的には改善していきたい部分ではあります。

ミツカリのFeature Flagの例

ミツカリではRuby(Ruby on Rails)とTypeScript(Next.js)を利用しています。

module FooFlag
  def self.enabled?
    Rails.env.local?
    # 開発が終了してSTG環境でも有効化する場合
    # Rails.env.local? || Rails.env.staging?
    # QAが終了してリリースする場合
    # true
  end
end

Rails.env.local? はRails 7.1からの機能でdevelopment or testです。

TypeScriptコードは割愛しますが、ほぼ同じような要領です。NEXT_PUBLIC_ENVIRONMENTという環境変数を各環境ごとに切り替えているので、その値と現状の設定値(どの環境まで許可するか)を比較することで実現しています。

このFlag moduleを各所で利用します。

controller PostsController < ApplicationController
  def create
    post = Post.create!(params[:post])

    if FooFlag.enabled?
      notify_to_slack('postが作成されました。')
    end

    render json: post
  end
end

少し雑な例ですが、このFlagモジュールの enabled? はdevelopmentまたはtest環境でのみtrueを返すため、このコードをリリースしてもSTG/PRD環境で notify_to_slack が動くことはありません。

ControllerやModel以外にもroutes.rbに書いて、そもそもrouting自体を無かったことにするケースもあります。

私自身は2018年頃に初めてこの手法を知りました。教えてくれた方は元Googleの方なのですが、Googleでもこの方法は使われていたそうです。

シンプルでありながら良い手法だと感心した記憶があります。

起源までは調べていませんが、マーチン・ファウラーの記事は2010年ですし、かなり昔からあるテクニックのようですね。ただ、その頃から浸透していたような記憶は無いです。近年はよく聞くようになった印象があります。

ナイーブな実装の問題点

ミツカリでの例は上記の通りですが、これには以下のような問題があり、実際に課題に感じたこともあります。

問題点1. Feature Flagをtrueに設定し、リリースした直後に問題が発覚し、すぐに戻したいときにすぐに戻せない

上記の実装例の場合、commit(revert)およびその後のCIやDeployが必要となってしまいます。すぐにフラグを元に戻せません。

この問題についてはFlagの値をDBに格納することで一定解決はできそうです。ただし、DBからのフェッチが増えるため、全体的なレイテンシの悪化が若干気になります。キャッシュするという考えが思いつきますが、その場合、パージのタイミングを考えなければいけませんし、仮にしない場合はキャッシュのTTLがフラグを戻せない時間になります。

また、フロントエンドは何らかのAPI等を経由してフラグの値を動的にロードする必要がありますが、SPA等の場合、どのタイミングでリフレッシュするのかというような問題もあります。

DBではなく、環境変数化するという考え方もあると思います。例えばAWSではパラメータストアなどに値を格納して挙動を変更する事ができると思いますが、アプリケーションサーバーの再起動が必要ではあります。commitから変更というよりはだいぶマシですが。Next.jsでSSGを使っている場合は環境変数はビルド時に利用されて、ランタイム時には使われないので、アプリケーションサーバーを再起動しても意味がないという問題もあります。

問題点2. Release Toggleは実現できているが、Permission Toggleは実現できていない

上記の実装例の場合は、テナントの判定が入らないため、本番環境で自社のテナントだけ有効化して挙動を確認してから全体展開するというようなことができません。個社対応などもできません。

問題点3. 複数のアプリケーション・プロジェクト間でフラグを共用できない

弊社ではモノリポの形式を取っており、1つのgitリポジトリ内にbackend(Rails)とfrontend(Next.js)の2つが存在し、当然ながらそれぞれ別のプロセスとして動いています。

RubyとTypeScriptで共通でコードは利用できないので、Feature Flagはそれぞれに定義することとなります。このときに開発者が同じでないためにフラグの名称が異なる、不要になったフラグのうちbackendだけ消してfrontendを消し忘れる、片方有効化を忘れるといったミスが発生します。

Feature Flagを何らかの別言語やASTで定義してcode generateするなど上手く作りこむなどすれば共通化できるのかもしれませんが、そこまで作り込みたくありません。

問題点4. プログラミングコード以外の部分でFeature flagを利用できない

例えば弊社ではOpenAPIのYAMLファイルを用意しており、ここからコード生成を行ったり、Runtime時にValidationを実行するようにしています。

このYAMLファイルもFeature Flagで管理したいと思うときがあります。例えばAPIのResponse body schemaが変更になるような場合、OpenAPIとController(Serializer)をFlagによって管理したいと思いますが、それはできません。

※この問題は後述するFeature FlagのSaaSやOSSを使っても解決できないと思います。git flowなどのブランチ戦略であればこの問題は起きないと思うので、その点はgit flowの方が良いですね。もしgit flow以外で良いやり方をご存じの方は@tsukaby0まで教えて下さい。


これらの問題を解決する手法がいくつかあります。1つはナイーブな実装をやめて、スマート・高度な実装に切り替えることです。

もう一つはSaaS・OSSを利用することです。

Feature FlagのSaaS LaunchDarkly

LaunchDarklyはFeature Flagのサービスです。

私の知り合いのλ沢さんが既にこちらの記事を書かれているので参考になります。

https://zenn.dev/microcms/articles/feature-flags-control-by-launchdarkly-basic

前述したナイーブな実装のFeature Flagの欠点は現時点でそれほど困っているわけではないですが、解決できれば嬉しいです。そこでLaunchDarklyに移行できないか検証してみました。

SDKをbundleするコストや、学習コスト、金銭コストなどはありますが、それほど難しいわけではないようですし、なかなか良さそうです。パフォーマンスのあたりは気になっていましたが、上手く考えられてるようですね。

LaunchDarklyの利点

Feature Flagの欠点を前述しましたが、LaunchDarklyを使うとその欠点を解消できます。

  1. フラグを有効化する場合はWeb Dashboard上でボタンを押すだけでよく、再ビルドや再デプロイは必要ない (※SSG時にFlagを使うと固定になってしまうという問題は避けられないはずなので、そこはCSRかSSRに切り替える必要がありそうです)
  2. 特定のテナントやユーザーだけでフラグを有効化できる (Permission Toggleができる)
  3. 複数のアプリケーション間でフラグを共用できる
    1. 利用するSDKは言語ごとに別ですが、Web Dashboard上で同一のフラグ定義を使えるというメリットがあります
  4. Server Sent Event(SSE)とFastlyインスタントパージの仕組みによってフラグ変更時は200msでアプリ側に伝播する。キャッシュパージなどを考慮する必要がない

LaunchDarklyの導入

LaunchDarklyのサイトでサインアップすると初期の手順が表示されるので、その通りにやれば簡単に使えます。

私の場合はRubyというより、Railsで使いたいので、表示された手順通りではなく、以下のDocに従いました。

https://docs.launchdarkly.com/sdk/server-side/ruby

Railsについての補足もありますね。LD clientをsingletonとして保持しておく必要があるようです。

はじめに、LaunchDarklyのSDKを使うためにlaunchdarkly-server-sdkをGemfileに追加します。

次に以下のように config/initializers/launchdarkly.rb を作成します。

Rails.configuration.client = LaunchDarkly::LDClient.new(ENV['LAUNCHDARKLY_SDK_KEY'])

次に以下のようにcontrollerを用意します。routes等は省略しています。

class Api::V1::PostsController < ApplicationController
  def index
    feature_flag_key = 'sample-feature'

    context = LaunchDarkly::LDContext.create({
                                               key: 'example-user-key',
                                               kind: 'user',
                                               name: 'Sandy'
                                             })

    client = Rails.configuration.client
    flag_value = client.variation(feature_flag_key, context, false)

    if flag_value
      render json: { test: 'flag on' }
    else
      render json: { test: 'flag off' }
    end
  end
end

※feature_flag_keyのconstはもっと上手く管理しないといけませんが。

この状態で LAUNCHDARKLY_SDK_KEY=自分のkey rails s でサーバーを起動し curl http://localhost:3000/api/v1/posts を実行すると

{"test":"flag off"}

というレスポンスが得られます。ここでLaunchDarkly GUI上でFlagをONにすると、サーバー再起動無しでcurlの結果が変わることを確認できます。

動的にON/OFFできるのは最高ですね!

Environment

GUI上で確認できますが、初期状態ではTestとProduction環境が用意されています。

このあたりは以下の資料が参考になります。

https://docs.launchdarkly.com/home/account/environment

フラグは環境ごとに異なる設定値をもたせられるので、まずはDevelopmentだけで有効化しておき、全てが完成したらStagingで有効化して確認し、最後にProductionで有効化する、というような運用を取ることができます。

サインアップ後のフローで提示されるSDK keyはTestのものなので、必要に応じてDevelopmentやStatingを作成しつつ、それらのKeyを取得して、各環境で使い分ける必要があるようです。

Targetingによって特定のユーザーのみフラグを切り替える (Permission Toggle)

詳細は以下に資料がありますが、テナントやユーザーごとにフラグを切り替えることができます。

https://docs.launchdarkly.com/home/flags/target

色々とやり方があり、かなり柔軟性が高そうです。同時に複雑性が高く初期のキャッチアップに少し手間取りそうですが、概念としてはそれほど難しくないようでした。

今回の例では in-house-user という適当な名称でセグメントを作り(社内ユーザーのセグメント)、そのセグメントに対してだけフラグをtrueにしてみたいと思います。

何らかのリリース時にまず社内ユーザーだけで使ってみて、OKであればリリースするというようなケースで使えそうです。

まず、コードを以下のように変更します。

class Api::V1::PostsController < ApplicationController
  def index
    feature_flag_key = 'sample-feature'

    # 1から10のランダム値
    context_data = if rand(1..10) >= 5
                     {
                       key: 'example-user-key1',
                       kind: 'user',
                       name: 'Sandy',
                       account_id: 1
                     }
                   else
                     {
                       key: 'example-user-key2',
                       kind: 'user',
                       name: 'Bob',
                       account_id: 2
                     }
                   end

    context = LaunchDarkly::LDContext.create(context_data)

    client = Rails.configuration.client
    flag_value = client.variation(feature_flag_key, context, false)

    if flag_value
      render json: { test: 'flag on', user_name: context_data[:name] }
    else
      render json: { test: 'flag off', user_name: context_data[:name] }
    end
  end
end

実際にはDBだとか、認証情報だとか、何かしらからリクエストユーザー・ログインユーザーを特定すると思いますが、今回はランダムで切り替えるようにしています。

account_idはユーザーIDやテナントIDなどと読み替えてください。今回の例ではaccount_idが1のユーザー(Sandy)は社内ユーザー、2のユーザー(Bob)は社外ユーザーとしています。

コードを変更したらこの状態で先に何度かアクセスし、コンテキストデータをLaunchDarklyに送信しておきます。

そうすると新たに送信した account_id というものが選べるようになっているので、以下の画像のようにセグメントおよびターゲティングを設定します。

[f:id:mitsucari:20250206094208p:plain]

これは in-house-user というセグメントであり、 account_id が1のユーザー、という設定です。

これを特定のフラグに紐づけます。

[f:id:mitsucari:20250206094211p:plain]

フラグが有効化されておりコンテキストがセグメントにマッチしている場合、つまり社内ユーザーの場合はtrueを、そうでない場合はfalseを返すようになります。

以下はcurlの結果です。意図通りになっていることが確認できます。

$ curl http://localhost:3000/api/v1/posts
{"test":"flag off","user_name":"Bob"}

$ curl http://localhost:3000/api/v1/posts
{"test":"flag on","user_name":"Sandy"}

これでPermission Toggleが実現できました。

価格

https://launchdarkly.com/pricing/

Pricingページには $12/mo とありますが、同時に

for 1 monthly service connection or 1K monthly contexts

と書いてあります。

Service connectionは

Service connections are the number of microservices, replicas, and environments connected to LaunchDarkly for 1 month

という説明があります。

例えば1つのサービスを5つのサービスで構成しているマイクロサービスがあって、それがdev/stg/prdの3つの環境を持つ場合は、 単純計算で 5(services)*3(env)*12($)=180($/mo) ということになります。

Contextsは

Each client side user or device creates a context

という説明です。ほぼMAU(Monthly Active User)ですね。先程のコード例でリクエストユーザーの情報をContextとして作成し、SDKを使っていましたが、その部分でContextとして作成したパターンの数がこの device creates a context なのかなと思っています。また、それとは別にclient sideでもContextを作れると思いますが、その部分の数も入ってきて、それが Each client side user なのかなと思っています。このあたりの課金については詳しくないので、どなたか詳しい方がいらっしゃいましたら教えていただけると嬉しいです。

or と書かれているのでService connectionとContextsで高いほうが請求額として決定されるのかなと思っていますが、使い込んでいないので詳細は分かりません。

つまりモノリシックなサービスでMAUが少ないBtoBのシステムだとかは費用を抑えられる可能性がありますね。逆にMicroservices構成やtoCでMAUが多い場合は高く付きそうです。

比較

LaunchDarklyの競合はいくつかあるようです。

LaunchDarklyがおそらく業界TOPであり、価格設定は高いのかなと思っています。それぞれ調べて自社の要件に合うところを探すと良い気がしますね。

OpenFeatureという選択肢

ここまで自社でのFeature Flagの実装例やSaaSについて述べましたが、OSSを利用するという方法もあります。

特定のSaaSを使うのもよいですが、ベンダーロックインされるというデメリットもあります。そこでこの業界ではOpenFeatureという標準規格が定められています。

https://openfeature.dev/

OpenFeatureはI/Fです。実装を用意したOSSがいくつかあります。また、LaunchDarklyなどもOpenFeatureに対応しており、自社の独自APIを使うのではなく、OpenFeatureのAPIを使いつつ実装はLaunchDarklyとすることができます。

つまりLaunchDarklyを利用しつつもOpenFeature経由で操作することでいつでも製品を乗り換えるということができます。(もちろんLaunchDarklyの独自機能が使えないなどのメリットはあるでしょうが)

このあたりのソフトウェアアーキテクチャについてはこちらを見るとイメージが付くかなと思います。

OpenFeatureのI/F, SDK, コード例

まずI/Fと言いましたがSDKは用意されています。例えばRubyの場合は

https://github.com/open-feature/ruby-sdk

こちらのSDKを利用します。以下はコードの例です。

require 'open_feature/sdk'
require 'json' # For JSON.dump

# API Initialization and configuration

OpenFeature::SDK.configure do |config|
  # your provider of choice, which will be used as the default provider
  config.set_provider(OpenFeature::SDK::Provider::InMemoryProvider.new(
    {
      "flag1" => true,
      "flag2" => 1
    }
  ))
end

# Create a client
client = OpenFeature::SDK.build_client

# fetching boolean value feature flag
bool_value = client.fetch_boolean_value(flag_key: 'boolean_flag', default_value: false)

コード上で OpenFeature::SDK::Provider::InMemoryProvider となっている部分が実装と言っている部分です。OpenFeatureのアーキテクチャ的にはProviderと呼びます。この部分にInmemoryを使うこともできるし、何らかのOSSを使うこともできるし、LaunchDarklyを使うこともできます。

OpenFeatureのOSS(Provider)

具体的にどのようなProviderを使えるかというとLaunchDarkly以外にも以下のような選択肢があります。(※このあたりは検証できていないため、不正確な情報が含まれている可能性があります)

Flagsmith

https://www.flagsmith.com/

FlagsmithはSaaSでLaunchDarklyよりも安価です。また、以下の通りOSS版も存在します。

https://github.com/Flagsmith/flagsmith

以下のKaiTakebeさんの記事では実際にFlagSmithをDockerで動かして利用されていますので、参考になります。

https://zenn.dev/tkb/articles/ffc832c7b35002

AWS AppConfig

AWS AppConfigの詳細は割愛しますが、アプリケーションの設定をAWS上(Web Dashboard上)で管理できるものだと思って構いません。

残念ながら公式だったりStarの多いAppConfig Providerは存在しません。

以下のkotobukiさんの記事では実装例が示されています。Providerを自分で実装してしまうのも一つの手ですね。

https://qiita.com/kotobuki5991/items/f1830d3562307f224e0b#openfeature-provider%E3%81%AB%E3%81%AFappconfig%E5%AF%BE%E5%BF%9C%E3%81%AEprovider%E3%81%8C%E3%81%AA%E3%81%84

Providerを自分で実装する根気や力がある方の場合、極端な話好きなストレージを選択できると言えそうです。DBだったり、KVSだったり、オブジェクトストレージだったり、ファイルシステムだったり。

GO Feature Flag

https://gofeatureflag.org/

GOと言っていますがGo-langで実装されているというだけで他の言語でも使えます。

以下のサイボウズの川向さんの記事ではより詳細に解説されてます。

https://blog.cybozu.io/entry/2024/08/15/080000

GO Feature FlagはYAMLなどのファイルベースで設定を管理するのですが、個人的にはここは好みではなく、DBなどで永続化したいと思っています。

他には?

https://openfeature.dev/ecosystem

こちらのOpenFeature公式でエコシステムとして様々なSDK, Providerが掲載されています。ここから望みのものを探すか、自作すると良いでしょう。

結論自分はどうするか、どうしたか

タイトルからちょっと外れますが現時点の結論としてはSaaSもOpenFeature(OSS)も使わずに今まで通りのナイーブな実装で十分だという考えに至りました。

  • 追加の学習コスト、SaaSコストをかけたくない
  • OSSでセルフホスティングして構築コスト、保守コストをかけたくない
  • OSSセルフホスティングの場合、スループットやレイテンシ、信頼性等の考慮が面倒
  • 障害点を増やしたくない (OSSでセルフホスティングでもSaaSでも依存が増えるため障害点は増える)
  • Permission Toggleは魅力的だが、どうしてもやりたいわけではない
  • 即時ロールバックできると確かに良いが、発生頻度を考えると今やるべきではない

いつかはPermission Toggleをもっと増やしたいという意見やリリース時のロールバック時の速度を改善したい、MTTRを短縮したいなどの意見が出てくるかなと思っています。そういう場合は再度適切に考えてバランスの良いソリューションを選択したいと思います。

ミツカリではこのようなテクノロジーマネジメントや技術選定、バランスを考慮した製品づくりを一緒にやってくれる仲間を募集しています!興味のある方はぜひお気軽にご連絡ください!

https://herp.careers/v1/mitsucari