OpenFeatureによるトランクベース開発
これは株式会社TimeTree Advent Calendar 2024の8日目の記事です。
はじめに
フィーチャーフラグは新機能のリリースを制御する強力なツールですが、その管理方法によってはチームの生産性に影響を与える可能性があります。自分が開発を担当しているTimeTreeウェブ版では、トランクベース開発(フィーチャーブランチを作らず、単一のメインブランチにコードをマージする手法)を採用しており、環境変数を使った単純なフラグ制御で新機能の開発を進めてきました。
以下は、これまで使用していたフラグ制御のコードのイメージです:
// 特定環境でのみオンになるフラグ
const betaFlag = process.env.beta
if (betaFlag == true) {
// ex) 新機能の処理
} else {
// 既存の処理
}
この方法はシンプルで理解しやすい一方で、複数の機能を同時に開発する際の管理が難しく、リリース時のトラブルシューティングも複雑になりがちでした。これらの課題を解決するため、OpenFeatureの導入に踏み切りました。
本記事では、その導入の経緯と実装について記します。
フィーチャーフラグ管理の課題
上記の運用には、主に2つ面倒なケースがありました。
1. 新機能をリリースでバグを生みやすい
複数の新機能を同時に開発している場合、各機能ごとにbetaFlagを参照するため、今回リリースする機能に関連するbetaFlagの発見難易度が上がります。間違ったbetaFlagを消してしまったら目も当てられません。
const betaFlag = process.env.beta
if (betaFlag) {
// 新機能1の処理
} else {
// 既存の処理
}
// betaFlagの責務が増えていく...
if (betaFlag) {
// 新機能2の処理
} else {
// 既存の処理
}
2. リリース後の対応の複雑さが上がる
もう一つは、障害やバグが見つかった際にリリースを切り戻す時。基本的にリリースPR(betaFlagを削除するPR)をRevertするだけですが、タイミングによってはコンフリクトが発生してしまいます。
一つひとつの負担は少ないのですが、最近たまたま両ケースに遭遇してエラい目にあいました。
これは改善せねばということで、本題であるOpenFeatureの導入をしました。
ボツ案
ちなみに初期案は、betaフラグを別の変数に格納するという素朴な実装でした。
// flag.ts
export const HOGE_FEATURE = process.env.beta;
export const FUGA_FEATURE = process.env.beta;
// sample.ts
import { HOGE_FEATURE, FUGA_FEATURE } from "~/flag.ts"
if (HOGE_FEATURE) {
// ex) 新機能の処理
} else {
// 既存の処理
}
if (FUGA_FEATURE) {
// 新機能2の処理
} else {
// 既存の処理
}
そんな折にメンバーがOpenFeatureを提案してくれて、オーバースペック気味ではあるが将来的にフィーチャーフラグ機能をしっかり入れるかも、という期待も含めて導入を決意しました。
OpenFeatureとは
さて、そもそもOpenFeatureとは何なのか。
OpenFeatureは、フィーチャーフラグの実装を標準化することを目指したオープンソースプロジェクトです。様々なプログラミング言語やプラットフォームに対応したSDKを提供しており、LaunchDarklyやSplit.ioなどの商用フィーチャーフラグサービスとの互換性も備えています。
- 標準化されたAPI 異なる言語やプラットフォーム間で一貫したフィーチャーフラグの実装が可能です。将来的にフラグ管理システムを変更する際も、アプリケーションコードの変更を最小限に抑えられます。
- プロバイダの柔軟性 インメモリでの単純な実装から、複雑な条件分岐を持つ本格的なフィーチャーフラグシステムまで、ニーズに応じた実装が可能です。
- 型安全性 TypeScriptのサポートにより、フラグの定義から使用まで型安全に実装できます。
OpenFeature導入編
基本の流れは公式ドキュメントの通りです。
Reactアプリケーションへの導入なので、ReactのDocを参照します。
まずはインストール。
npm install --save @openfeature/react-sdk
# or
yarn add @openfeature/react-sdk @openfeature/web-sdk @openfeature/core
次にフラグを定義します。
環境変数のbetaフラグがtrueならオンになるよう設定するために、動的にフラグを制御できるcontextEvaluator
プロパティを使っています。
// flagConfig.ts
import type { InMemoryProvider } from "@openfeature/react-sdk";
type FlagConfiguration = ConstructorParameters<typeof InMemoryProvider>[0];
export const OPEN_FEATURE_CONFIG = {
"hoge-feature": {
disabled: false,
variants: {
on: true,
off: false,
},
defaultVariant: "off",
contextEvaluator: (_) => process.env.beta ? 'on' : 'off',
},
} as const satisfies FlagConfiguration;
次にアプリケーションへのつなぎこみです。使用箇所をProviderで囲みます。基本的にはRedux等のProviderと同じように大元のコンポーネント(App.tsx
等)で囲ってあげればよいと思います。
// App.tsx
import {
InMemoryProvider,
OpenFeature,
OpenFeatureProvider,
} from "@openfeature/react-sdk";
import { OPEN_FEATURE_CONFIG } from "~/flagConfig";
OpenFeature.setProvider(new InMemoryProvider(OPEN_FEATURE_CONFIG));
export const App = () => {
return (
<OpenFeatureProvider>
<MyComponent />
</OpenFeatureProvider>
);
};
今回は取り扱いませんが、複数のProviderを使い分けることもできます。
OpenFeature React SDK
あとは使用する場所でフックから呼び出せばフラグを参照できます。
// HogeComponent.tsx
import { useBooleanFlagValue } from "@openfeature/react-sdk";
const hogeFeatureFlag = useBooleanFlagValue("hoge-feature");
const HogeComponent = () => {
if (hogeFeatureFlag) {
return <NewComponent />;
} else {
return <OldComponent />;
}
};
おまけに、useBooleanFlagValue
を改善します。
現状だと引数の型がstring
になっているので、文字列リテラルのユニオン型になるよう薄いラッパーを作ります。
// lib/openfeature/useBooleanFlagValue.ts
import { useBooleanFlagValue as _useBooleanFlagValue } from "@openfeature/react-sdk";
import type { OPEN_FEATURE_CONFIG } from "~/flagConfig";
/**
* フラグの値を取得します。
* @param flagKey フラグのキー
* @param defaultValue フラグが取得できなかった場合に返す値
* @param options オプション
* @returns フラグの値
* @example
* const hogeFeatureFlag = useBooleanFlagValue("hoge-feature");
* hogeFeatureFlag => true
*/
export const useBooleanFlagValue = (
flagKey: keyof NonNullable<typeof OPEN_FEATURE_CONFIG>,
defaultValue: Parameters<typeof _useBooleanFlagValue>[1] = false,
options?: Parameters<typeof _useBooleanFlagValue>[2],
) => _useBooleanFlagValue(flagKey, defaultValue, options);
// HogeComponent.tsx
import { useBooleanFlagValue } from "~/lib/openfeature/useBooleanFlagValue.ts";
// hoge-feature以外は型エラーになる
const hogeFeatureFlag = useBooleanFlagValue("hoge-feature");
const HogeComponent = () => {
// 省略
};
運用について
基本的にフラグはprocess.env.beta
がtrue
ならオン、false
ならオフで、環境ごとの機能の出し分けをしています。
// 再掲
// flagConfig.ts
import type { InMemoryProvider } from "@openfeature/react-sdk";
type FlagConfiguration = ConstructorParameters<typeof InMemoryProvider>[0];
export const OPEN_FEATURE_CONFIG = {
"hoge-feature": {
disabled: false,
variants: {
on: true,
off: false,
},
defaultVariant: "off",
contextEvaluator: (_) => process.env.beta ? 'on' : 'off',
},
} as const satisfies FlagConfiguration;
いざリリースとなった際は、contextEvaluator: 'on'
にしてリリース。
リリース後にバグが発見されたりして切り戻す際はcontextEvaluator: 'off'
にします。
リリース後しばらく様子を見て問題がなかったら、フラグごと消すという運用にしてます。
ちゃんとしたフィーチャーフラグ機能を導入している場合は、このオン・オフ操作はリモートでできると思うので、現状はとても原始的ですね。
さいごに
まだ運用して日は浅いですが、当初の困りごとは無事に解決しました。
コンフィグの書きぶりなどは少し冗長に感じますが、独自実装するよりは標準(を目指してる)仕様に沿った安心感はあります。
これをきっかけにフィーチャーフラグ機能を導入する機運も高まったので、いい起爆剤になったかなと思います。
おまけ
フィーチャーフラグの歴史
フィーチャーフラグが注目され始めたのは2000年代後半。FacebookやGitHubといった企業が、デプロイしたコードの振る舞いを動的に変更できる仕組みを導入しはじめました。このときは「dark launching(ダークローンチ」とも呼ばれていました。
2010年代に入ると、継続的デリバリー(CD)の普及とともにフィーチャーフラグの重要性も高まりました。コードは常にメインブランチにあるべきという考え方が主流になるにつれ、フィーチャーフラグの重要性は更に増しました。
フィーチャーフラグは単なる機能のオン・オフだけでなく、A/Bテストやカナリアリリース、パーソナライゼーションまで様々な用途に発展していきました。最初は開発者の便利ツールだったものが、今やビジネス戦略の重要な要素になっているわけです。いい話ですね。
参考文献
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion