tRPC x Feature Flag で実現するスムーズなトランクベース開発
はじめに
こんにちは!パブテクでテックリードをしているyoshikiです。
今回は弊社新規プロダクトでFeatureFlagを導入することになったのでtRPCと組み合わせて安全にトランクベース開発を運用していく方法を記載しました。
FeatureFlagを導入することで「顧客に迷惑をかけることなく安全に新機能をリリースできる」「mainへのマージでコンフリクトを少なくしたい」今回紹介するtRPCのmiddlewareを利用することで「フラグの分岐修正ミス」などの課題を解決できるかと思います。
本題
今回は、tRPC と Feature Flag を組み合わせて、トランクベース開発をスムーズに進める方法についてご紹介します。
トランクベース開発では、全開発者が単一のブランチ(通常は main ブランチ)で作業を行い、頻繁に統合することでコードの衝突を減らし、開発のスピードを向上させます。しかし、未完成の機能をどうやってユーザーから隠しながら本番環境にデプロイするか?という課題が存在します。
この記事では、tRPC のミドルウェア機能を使って、API レベルでの Feature Flag 制御を実装する方法を解説します。
Feature Flag とは?
Feature Flag(機能フラグ)は、アプリケーションのコード内に条件分岐を設け、特定の機能のオン・オフを動的に切り替える手法です。これにより以下のようなメリットが得られます:
- 未完成の機能を本番環境にデプロイしたままで、利用できないように制御できる
- 特定のユーザーやテナントに対してのみ新機能を公開できる(A/Bテストなど)
- 問題が発生した場合に、コードをロールバックせずに機能を無効化できる
トランクベース開発において、Feature Flag は「完成していないコードを main ブランチに入れる」という課題を解決するための重要なツールとなります。
Feature Flag サービスの比較
Feature Flag を実装するには、自前で構築する方法と、既存のサービスを利用する方法があります。以下に主なサービスの比較を示します:
比較表
サービス | 無料プラン (制限) | 最安有料プラン (価格) | 商用利用 | オープンソース版 (セルフホスト) | OpenFeature対応ライブラリ |
---|---|---|---|---|---|
LaunchDarkly | あり(Developerプラン: 無料「Free forever」。1プロジェクト、3環境、5接続/月、クライアント側MAU 1,000/月) | Foundationプラン: _従量課金_で$12/サービス接続または$12/1,000 MAUごと(Unlimited席、プロジェクト・環境無制限) | 可能(無料枠を含め商用利用可) | 無し(クローズドソースのSaaS) | LaunchDarkly OpenFeature Provider |
Flagsmith | あり(Freeプラン: 50,000リクエスト/月、1チームメンバー、フラグ・環境・ユーザーセグメント無制限) | Start-Upプラン: $45/月(~1,000,000リクエスト/月、3ユーザーまで) | 可能(無料プラン含め商用利用可) | あり(OSS版を提供。BSD-3-Clauseライセンス) | Flagsmith OpenFeature Provider |
Unleash | あり(OSS版を無料セルフホスト可能。クラウド版の無料プランなし) | Proプラン: $80/月(5ユーザーまで含む。2環境・3プロジェクトまで、追加ユーザーは1人$15/月) | 可能(OSS版はApache License 2.0で商用利用可) | あり(Apache2.0ライセンスのOSS版を提供) | Unleash OpenFeature Provider |
Split (Harness) | あり(Developerプラン: 10ユーザーまで無料、月間ユニークユーザ指標MTK_50,000_まで) | Teamプラン: $33/ユーザー/月(10~25ユーザー向け。月間~50,000ユニークユーザまでを含む) | 可能(無料Developer版を商用利用可) | 無し | Split.io OpenFeature Provider |
CloudBees | あり(Communityエディション: 最大5ユーザー、100,000 MAUまで無料) | Teamプラン: 16~25ユーザー・最大100万MAUまで対応。例: ~20ユーザー・50万MAUで$1,325/月 | 可能(無料版も機能フルセットで提供) | 無し | CloudBees OpenFeature Provider |
Optimizely Rollouts | あり(Rolloutsプラン: 無料で無制限のフラグとコラボレータ利用可能。※同時実行実験は1件まで) | Full Stackプラン: エンタープライズ向け有料版(座席数やMAU増加・高度な機能には有料Full Stackへのアップグレードが必要) | 可能(全ユーザー・企業に無料提供) | 無し | Optimizely OpenFeature Provider |
ConfigCat | あり(Freeプラン: 2環境・10フラグまで) | Proプラン: $99/月〜(フラグ・環境数の上限増) | 可能(無料でも機能制限なく商用利用可) | 無し(※Enterprise契約でソースコード共有のエスクロー提供あり) | ConfigCat OpenFeature Provider |
DevCycle | あり(Free Forever: 無制限Seats、1,000 MAU/月まで無料) | Developerプラン: $10/月 ※年払(1,000 MAU含む) | 可能(無料版を継続利用可能) | 無し(OpenFeature標準採用の独自クラウドサービス) | DevCycle OpenFeature Provider |
これらの比較から、最終的に我々は CloudBees を採用することにしました。主な採用理由は:
- 10万MAUまで無料で使える
- OpenFeature Provider がサポートされている
- バックエンドでの利用が主なユースケースだった
OpenFeature とは?
OpenFeature は、Feature Flag のオープンスタンダードを目指すプロジェクトです。異なるベンダーの Feature Flag サービスを統一的な API で扱えるようにすることで、ベンダーロックインを防ぎ、将来的な乗り換えをスムーズにします。
CloudBeesをはじめ、多くの Feature Flag サービスが OpenFeature に対応したプロバイダーを提供しています。
tRPC ミドルウェアによる Feature Flag の実装
では、実際に tRPC と Feature Flag を組み合わせた実装を見ていきましょう。
1. 必要なパッケージのインストール
npm install @openfeature/server-sdk cloudbees-openfeature-provider-node
2. OpenFeature ゲートウェイの実装
まず、OpenFeature の API を使って Feature Flag を評価するゲートウェイクラスを作成します:
// OpenFeatureGateway.ts
import {
type Client,
type EvaluationContext,
type JsonValue,
OpenFeature,
} from "@openfeature/js-sdk";
import { CloudbeesProvider } from "cloudbees-openfeature-provider-node";
import { injectable, singleton } from "tsyringe";
import type { IOpenFeatureGateway } from "./types";
@injectable()
@singleton()
export class OpenFeatureGateway implements IOpenFeatureGateway {
private defaultClient: Client | null = null;
private initialized = false;
/**
* ゲートウェイを初期化し、OpenFeatureにプロバイダーを設定
*/
public async initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
// OpenFeatureにプロバイダーを設定して初期化完了を待つ
await OpenFeature.setProviderAndWait(
await CloudbeesProvider.build(process.env.FEATURE_FLAG_API_KEY || ""),
);
this.defaultClient = OpenFeature.getClient();
this.initialized = true;
} catch (error) {
console.error("Failed to initialize OpenFeature Gateway:", error);
throw error;
}
}
public async close(): Promise<void> {
if (!this.initialized) {
return;
}
await OpenFeature.close();
this.defaultClient = null;
this.initialized = false;
}
/**
* ブールフラグの値を評価
* @param flagKey フラグのキー
* @param defaultValue デフォルト値
* @param context 評価コンテキスト
*/
public async getBooleanValue(
flagKey: string,
defaultValue: boolean,
context?: Record<string, any>,
): Promise<boolean> {
await this.ensureInitialized();
if (!this.defaultClient) throw new Error("Default client not found.");
return this.defaultClient.getBooleanValue(
flagKey,
defaultValue,
context as EvaluationContext,
);
}
// その他のメソッド(getStringValue, getNumberValueなど)は省略
}
3. Feature Flag サービスの実装
次に、アプリケーション内で使用するサービスクラスを作成します:
// FeatureFlagService.ts
import { inject, injectable } from "tsyringe";
import type { IOpenFeatureGateway } from "../../infrastructure/externalapi/openfeature/types";
@injectable()
export class FeatureFlagService {
constructor(
@inject("IOpenFeatureGateway")
private openFeatureGateway: IOpenFeatureGateway,
) {}
async getBooleanValue(flagKey: string, userId: string, tenantId: string) {
return this.openFeatureGateway.getBooleanValue(flagKey, false, {
user_id: userId,
tenant_id: tenantId,
});
}
}
4. tRPC ミドルウェアの実装
Feature Flag を評価するミドルウェアを実装します。これが今回の記事の肝です
// featureFlag.ts
import { TRPCError } from "@trpc/server";
import { FeatureFlagService } from "../application/service/FeatureFlagService";
import { container } from "../container";
import { t } from "../trpc";
import { isAuthenticated } from "./auth";
const featureFlagService = container.resolve(FeatureFlagService);
/**
* 認証済みかつ特定のフィーチャーフラグが有効な場合のみ処理を許可するprocedure
* @param flagKey フィーチャーフラグのキー
* @returns 認証とフィーチャーフラグをチェックするprocedure
*/
export const featureFlagProcedure = (flagKey: string) => {
return t.procedure.use(isAuthenticated).use(async ({ ctx, next }) => {
// テナントからフィーチャーフラグの状態を確認
const tenant = ctx.tenant;
if (!tenant) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message: "Tenant information is required for feature flag check",
});
}
// フィーチャーフラグの状態を確認
const isEnabled = await featureFlagService.getBooleanValue(
flagKey,
ctx.userId,
tenant.tenantId,
);
if (!isEnabled) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Feature '${flagKey}' is not enabled for this tenant`,
});
}
// フィーチャーが有効な場合は次の処理に進む
return next({
ctx: {
...ctx,
},
});
});
};
このミドルウェアは、特定の機能フラグが有効になっている場合のみ、API エンドポイントへのアクセスを許可します。ポイントは:
- 認証済みユーザーのみがアクセスできる(**
isAuthenticated
**ミドルウェアを使用) - テナント情報を取得して、そのテナントに対して機能が有効かどうかを確認
- 機能が無効な場合は
FORBIDDEN
エラーを返し、有効な場合は次の処理に進む
あとは、フラグをcloudbeesの管理画面で作成してテナントやユーザIDで条件を作成して確認をしてみましょう。
5. ルーターでの使用例
作成した**featureFlagProcedure
**を使って、特定の API エンドポイントに Feature Flag を適用します:
// TestRouter.ts
import { router } from "../../middleware";
import { featureFlagProcedure } from "../../middleware/featureFlag";
export const testRouter = router({
// この API は "new_feature" が有効な場合のみアクセス可能
testFeatureFlag: featureFlagProcedure("new_feature").query((ctx) => {
return "new_feature is enabled";
}),
// 他のエンドポイント...
});
6. サーバー起動時の初期化
最後に、サーバー起動時に Feature Flag サービスを初期化します:
// server.ts
const start = async () => {
try {
// Initialize feature flags
const featureFlagGateway = container.resolve<IOpenFeatureGateway>(
"IOpenFeatureGateway",
);
await featureFlagGateway.initialize();
await server.listen({ port: Number(port), host: "0.0.0.0" });
console.log("listening on port", port);
// Cleanup on shutdown
const cleanup = async () => {
await featureFlagGateway.close();
await stop();
};
process.on("SIGTERM", cleanup);
process.on("SIGINT", cleanup);
} catch (err) {
server.log.error(err);
process.exit(1);
}
};
この実装のメリット
この tRPC ミドルウェアによる Feature Flag 実装には、以下のメリットがあります:
- API レベルの一元管理: 機能のオン・オフを API レベルで制御できるため、フロントエンドでの条件分岐が不要になります。
- 型安全性: tRPC の型安全性を活かして、Feature Flag の状態に基づいた API の可用性をコンパイル時に確認できます。
-
メンテナンス性: 新機能の追加時に
featureFlagProcedure("new_feature")
と書くだけで簡単に Feature Flag が適用できます。 - featureFlagを消すときは
authProcedure
に戻すだけなのでif分岐に悩む必要がありません。 - スケーラビリティ: OpenFeature を採用しているため、将来的に別の Feature Flag サービスに乗り換えることも容易です。
まとめ:トランクベース開発と Feature Flag
トランクベース開発と Feature Flag を組み合わせることで、以下のようなメリットが得られます:
- 長期間のブランチ開発による統合の痛みを避けることができる
- 本番環境に未完成のコードをデプロイしつつ、ユーザーには見せない制御が可能
- 段階的なロールアウトやA/Bテストなど、より高度なデプロイ戦略が取れる
特に、tRPC のミドルウェア機能を活用することで、型安全かつ一元管理された Feature Flag の実装が可能になります。これにより、開発者はコードの品質を維持しながら、より速いペースで機能を提供できるようになります。
前の職場では自前のDBでfeatureFlagを管理していましたが、マイクロサービス時代ではFeatureFlag SaaSを取り扱うことが当たり前になっていきそうですね。上手く規模に応じてSaaSを乗り換えることもできるのでOpenFeatureを選定して良かったと思っています。
私たちのプロジェクトでは CloudBees と OpenFeature を採用しましたが、OpenFeature の標準化により、将来的な要件変更にも柔軟に対応できる体制が整いました。トランクベース開発を実践する上で、Feature Flag は非常に強力なツールとなっています。
ぜひ皆さんのプロジェクトでも、tRPC と Feature Flag を組み合わせた開発手法を試してみてください!
Discussion