Rettyアプリチームの @imaizume です、新しい年度が始まり新卒社員も入社してフレッシュな気分になる今日このごろですね。
今回は最近Rettyアプリチームで利用することが増えている、Feature Flagを使った開発についてのお話です。
Feature Flagを使うことで、大規模な開発であっても開発中から継続的に差分をマージできるようになり、デリバリー効率を大きく向上させることができます。
本記事では、この方法を採用するに至った背景や実際に取り入れて感じたメリット、課題感などを書きましたので、読んでみてメリットが感じられたならぜひみなさんの開発でも取り入れてみていただければと思っています。
- Feature Flagとは?
- FFを採用する前の開発の様子
- RettyでのFFによる開発の始まり
- アプリチームでFFを採用したことによる変化
- RettyアプリチームでのFF(RT)の具体的なやり方
- 課題点や今後について
- まとめ
Feature Flagとは?
ご存じない方のために、はじめにFeature Flag (以下FF) の概要をお話します。
FFは新機能や変更のリリースを行う際、直接コードを変更するのではなく分岐を使って有効化または無効化する開発運用方法のことです。
FFにもいくつか分類があるようですが、今回はアプリチームで運用されることの多いRelease Toggles(リリーストグル、以下RT)を中心にお話します。
RTはFFをリリースに利用することを指し、新しいコードを普段から少しずつフラグで隠した状態でリリース用ブランチへマージし、リリース時にフラグを反転させるだけで機能が瞬時に表出するというものです。
FFを採用する前の開発の様子
ではなぜRettyアプリチームでFFを採用したのかをお話したいと思います。
もともと2021年半ば頃まで、Rettyアプリの開発ではFFではなくFeature Branch方式(以下FB)を採用していました。
FBは、機能開発を行う場合に専用ブランチを作成し実装完了後リリースブランチ(以下RB)へとマージする方式で、現在も小規模の変更に対してはこの方式を採用しています。
他方直近1,2年のRettyでは、施策あたりの実装量が比較的大きいものが多く、1施策あたりの開発期間が長くなりがちという背景もありました。
実装量が多いということは当然FBとRBの差分も大きくなりがちで、また開発期間も長くなることでマージされるまでの期間も伸びていきます。
開発される施策が1つだけであればこれでも問題ありませんが、本筋の施策開発と並行しバグ修正やhotfixでのリリース、ライブラリ更新といった変更が発生することも少なくなく、競合が発生しないようこまめにRB → FBへのローカルマージが必要でした。
もちろん常にうまくマージできるわけではなく、時にはFB側で競合を都度解消する必要があるという点で運用コストが高いという課題があったのです。
例えばiOSであればxcodeprojやStoryboardファイルでの競合が主な例で、ときには競合解消の過程で間違って必要な実装した機能を消してしまいバグを生んだこともありました。
またhotfixで対応したタスクが1スプリント以上に渡った結果、終わってFBで作業を再開するためにさらに数日競合解消をすることになったというケースもありました。
ちなみにこのような、1つの機能に関連する全変更を混ぜたFBを大きな1つのPull Request(以下PR)にしてRBへマージする方式を、社内では「ビッグバンリリース」と呼んでいました。
RettyでのFFによる開発の始まり
そんな折、WebチームでFFを利用した開発がなされていることを知りました。
実はWebチームも2020年頃までFB方式での開発が一般的でしたが、下記のような理由から同年に行われたGo To Eatキャンペーンの開発よりFFを利用したリリースに切り替えたのでした。
- Go To Eatは社員の大半が数ヶ月かかりきりになる大型プロジェクトであった
- ビッグバンリリースはマージコストはもちろん障害発生時やキャンペーン終了対応も大変なため避けたかった
- 同プロジェクトの開始時から時限式のRTを実装することを決めた
この方式が見事に功を奏し成功体験が生まれたことで、今日まで頻繁に使われるようになりましたが、アプリチームはそれより少し遅れて2021年後半からFFを積極的に用いる開発を始めました。
アプリチームでFFを採用したことによる変化
それまでは新規の大型施策開発を開始する際は、対応する親のFBを作成しそこから小さなブランチを作成するという方針で開発していました。
しかしFFではFBを作らず、直接RBからブランチを切り、フラグで落とした状態で開発した個別の差分をRBへ取り込んでいきます。
こうすることで、次のようなメリット・改善点が生まれました。
マージコストの削減によるプロダクトデリバリー効率の向上
全体としては大きな変更量があるリリースでも、少しずつ素早く機能差分を混ぜていくことができるたため、前述したようなマージ時のコストが発生しなくなりました。
例えば「ある画面をフルリニューアルする」という施策の場合
- ViewやViewModelなどのコンポーネント定義は表出導線がないので本番に影響しない。
- 表出させる箇所や既存実装と共有される箇所にはFFを入れる。開発時だけ新実装側へ流しリリース時は旧実装に流すようにすることで本番に影響しない。
このようにすることで、長期間の開発でもリリースブランチ上に差分がマージされ、FBのようにマージのコストも低くて済みます。
私の体感でも、マージ時に競合が発生することは少なくなりましたし、途中で別の開発を行ったあとに作業を再開するときの追従コストが圧倒的に減りました!
こうした競合解消や追従はプロダクト価値には一切つながらない無駄なコストだったのだということを、今では身を持って体感しています。
コードレビュー負荷の低減と不要機能の可視化
FB型の開発では既存コードを大きく書き換えながら進めることになるので、単一のPRで差分が大きくなりレビュー難易度が上がりがちです。
一方FFを使う場合、Flagによる分岐の追加とFlagにより不要になったコード削除を別のPRに分けることができるため、差分が減ってレビューがしやすくなります。
同様の理由で、不要になったコードが明確になり古い実装が削除しやすいというメリットもあります。
他のブランチから/への影響を抑える
FB方式では、途中で別の施策やhotfix対応により作成したブランチをリリースブランチ経由で取り込む必要があり、そのことを考慮してFBのマージを待つというケースも散見されました。
一方FFを使う場合、個々のブランチは独立してマージが可能なため、他の開発ブランチを意識せずにガンガンマージすることができます。
逆に他のブランチへの影響も必要最小限に抑えられるため、開発人数が増え並列数が増えたとしてもスケールする開発手法であると言えるでしょう。
RettyアプリチームでのFF(RT)の具体的なやり方
ここまでFF方式を採用した背景とメリットについてお話しましたが、ここから具体的なコードを交えた開発運用方法についてご紹介します。
FFを使うにはライブラリを利用する方法もあるようですが、Rettyの場合はiOS Androidいずれにおいてもライブラリは利用しておらず自前で実装しています。
利用可能なFFの実装方法についてまとめられているサイトもあるので、導入の際はこちらも併せてご覧いただくとよいでしょう。
RettyでのFFの実装方法には以下の2つがあり、それぞれの特徴に応じた使い分けをしています。 時限式でリリースする必要がある場合を除き、基本的にはコード埋込み型が用いられます。
- コード埋め込み型フラグ
- フラグ反転はコード書き換えにより実現
- アプリの新バージョンリリースタイミングで有効化
- 機能表出に時間的制約がなくリリースタイミング時で良い場合に用いられる (主に通常の大型施策開発など)
- 後述のRemoteConfigを用いる方法よりも単純かつシンプル
- Firebase Remote Configを利用したフラグ
- フラグ反転はFirebase Console上から直接指定、またはJSONで期日を渡すことによって実現
- Firebase Consoleからのフラグの反転時、または期日になったタイミングで有効化
- 機能表出に時間的制約がありリリースタイミングに依存してほしくない場合に用いられる (主にキャンペーンの開始・終了など)
- Firebaseの仕様への依存などを考慮する必要がある
iOSでのコード埋込み型FF
はじめは1つの専用ファイルを用意し、大域変数としてフラグを定義するという極めてシンプルな方法で運用をスタートしました。
単に開発するだけであれば、これだけでも十分立派なFFです。
// 大域変数でFFを定義 let showNewRestaurantScreenEnabled = true
ただCIでのビルド時に外部からオプションとしてフラグの値を指定したいという要望が上がったたため、現在ではfastlaneでのビルド時に OTHER_SWIFT_FLAGS
経由でフラグを指定可能になっています。
そのためにはまずiOSプロジェクトのコード上で、以下のようにFFとプリプロセッサマクロを定義します。
プリプロセッサマクロには FEATURE_FLAG_
というプレフィックスをつけることでFFとして区別できるように命名に制約をもたせています。
#if FEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN let showNewRestaurantScreenEnabled = true #else let showNewRestaurantScreenEnabled = false #endif
このFFを利用して機能を開発していきます。
if showNewRestaurantScreenEnabled { // 新しい画面: FFをONにするまで表出しない let view = UIHostingController(rootView: NewView()) self.present(view, animated: true, completion: nil) } else { // 古い画面: FFがONになるまで表出し続ける let view = storyboard!.instantiateViewController(withIdentifier: "OldViewController") self.present(view ,animated: true, completion: nil) }
次にfastlaneの設定です。
build_app
のlaneに xcargs
という引数があり、ここに OTHER_SWIFT_FLAGS
に指定する値を渡すことができますので、 option
で受け取った値をここに渡すようにしています。
オプションを複数渡す場合はカンマ区切りで渡せるようにし、前述の FEATURE_FLAG_
は指定時に省略できるようにしています。
desc "iOSでbeta版を配信するためのlane" lane :distribute do |options| ... # FeatureFlags = HONYA,MORAKE と入ってくる。 `FEATURE_FLAG_` というプレフィックスをつけてotherSwiftFlagにする。 activate_feature_flags = (options[:activate_feature_flags] || '').split(",") xcodebuild_args = activate_feature_flags.map { |k| "-D FEATURE_FLAG_#{k.shellescape}" }.join(' ') build_app( workspace: "RettyApp.xcworkspace", scheme: "RettyApp", export_method: "ad-hoc", xcargs: xcodebuild_args.length > 0 ? "OTHER_SWIFT_FLAGS='$(inherited) #{xcodebuild_args}'" : "" ) ... end end
あとはfastlane実行時にoptionにフラグ名を指定することで、ビルド時に外部からFFを渡すことができるようになります。
bundle exec fastlane distribute activate_feature_flags=FEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN
RettyではSlack Botを自前のmacminiに常駐させているため、Slack経由でビルドを命令することができ、その場合も同様に引数でFFを受け付けるようにしています。
Androidでのコード埋込み型FF
Androidの場合はFFを build.gradle
に定義し、コマンドライン引数から値を指定します。
直接 build.gradle
に記述するのもありですが、ファイル分割をする場合は切り分けたgradleファイル(ここでは featureflags.gradle
とします)に次のように記載し
android { defaultConfig { buildConfigField "Boolean", "FEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN", rootProject.property("me.retty.flag.showNewRestaurantScreenEnabled") } }
本体からこれを読み込むようにします。
apply from: "featureflags.gradle"
そしてプロジェクト上で利用するには BuildConfig
経由でFFを取得します
if (BuildConfig.FEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN) { requireActivity().startActivity( Intent( activity, NewActivity::class.java ) ) } else { requireActivity().startActivity( Intent( activity, OldActivity::class.java ) ) }
FFの指定はコマンドライン引数から行います。AndroidStudioの Preferences | Build, Execution, Deployment | Compiler
の中にCommand-line Optionsという項目があるのでここにプロジェクトプロパティとして -P
をつけて指定します。
-PFEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN=true
これであとはgradleでのビルド時にも同様のオプションを指定することでCI等からもFFを指定可能になります。
./gradlew build -PFEATURE_FLAG_SHOW_NEW_RESTAURANT_SCREEN=true
Firebase Remote Configを使ったFF
最後にFirebase Remote Configを使った方法にも触れておきます。
基本的な仕様については公式ドキュメントなどを参考にしてください。
時限式でリリース/クローズするため、パラメータを定義したうえで日時条件を指定します。
例えば2022/01/01から2022/03/31までの期間限定で表示する場合は、下記のような条件を作成したうえでパラメータの条件値に設定してあげます。
こうすることで2021年12月中にアプリをリリースし、2022年になった時点で機能が表出するようになります。
注意点としてFirebase Remote Configはクライアントが一度fetchした場合にキャッシュが有効になります。
デフォルトでは12時間有効なため、一度パラメータを公開したあとに値(期日)を変更した場合には浸透まで時間がかかるので注意しましょう。
以上がRettyにおける具体的なFFの実装方法でした。
課題点や今後について
ここまでFFのメリットや実装方法についてお話してきましたが、最後に課題点についても述べておきます。
基本的にはFB方式(ビッグバンリリース)と比べてメリットは大きいのですが、FF固有の問題としてリリース後のお掃除タスクが発生するという点が挙げられます。
作成したFFは、リリース後は古い実装とともに不要な存在となります。
FFの場合はリリース時点で両実装が共存する状態のため、リリース後は必ず旧実装を削除するタスクが発生します。
これらを放置すると、長期的には不要なフラグや実装が増えてしまい不必要な考慮が発生したりコードの可読性が下がるなど、かえってコスト増加の要因となってしまいますので、なる早でお掃除をしてあげる必要があるでしょう。
これについてはRettyでも次の開発に着手する前または並行して旧実装の削除を行うようにしている他、最近ではGitHubが半自動でコードをクリーンする仕組みを導入し始めているという話も出ています。
Rettyでも今後は手動からこうした自動化の仕組みを利用するように移行し、本質的なプロダクトの価値向上のための開発により一層集中できるようにしていきたいと考えています。
また開発中に誤ってFFがONにならないよう、コードレビューでのチェックに加えてリリース前のQAにおいても表出を確認するという体制を取っています。
アプリチームは原則毎スプリント定期リリースを行うことになっており、本番に影響がでないように注意しつつ、開発未完了の機能も途中までできた分を積極的にマージしています。
まとめ
今回はRettyアプリチームでのFFによる開発についてご紹介しました。
FFを利用するメリットをまとめると次のようになります。
- マージコストの削減によるプロダクトデリバリー効率の向上
- コードレビュー負荷の低減と不要機能の可視化
- 他のブランチから/への影響を抑える
Rettyでは引き続き高速・高頻度な開発・リリースを目指してチームで改善に取り組んでいくとともに、また新たな取組があればこのブログでお伝えしていきたいと思います。
プロダクトグロースや食の領域に興味があるアプリエンジニアからのご応募も引き続きお待ちしております!
meetyもやっていますのでぜひお気軽にご応募ください!