背景と実際の課題
約2年前、私たちのチームにとって本番環境への新機能リリースは毎回かなりのプレッシャーでした。典型的なワークフローはこうです。コードをmainブランチにマージし、CI/CDをトリガーしてDockerイメージをビルドし、Kubernetesクラスタを更新します。しかし、もし運悪くメモリリークやPostgreSQLのテーブルロックを引き起こすクエリが紛れ込んでいれば、チーム総出でコミットをリバートし、ゼロから再ビルドを走らせる羽目になります。このロールバック処理には15〜20分もかかり、その間にも数百件の注文が中断されてしまっていました。
根本的な問題は、本来切り離すべき2つの概念を混同してしまっている点にありました。
- Deployment(デプロイ): 新しいソースコードをサーバー上で安全に実行可能な状態にすること。
- Release(リリース): その機能をエンドユーザーに公開して利用可能にすること。
この2つのステップを分離することこそが、Feature Flags(またはFeature Toggles)が生まれた理由です。1か月もブランチを抱え込む代わりに、チームの5人のエンジニアは毎日メインブランチへ頻繁にマージする手法(Trunk-based Development)へと移行しました。すべての新しいコードは、OFF状態のFeature Flagでラップされます。内部テストを行いたい場合や、5%のユーザーだけに試験公開したい場合、テックリードがUI上でフラグを切り替えるだけで済みます。インシデント対応の時間は20分から、わずか1秒・ワンクリックへと短縮されました。
ツールの選定において、LaunchDarklyは非常に快適に動作するものの、ユーザーあたり月額8〜10ドルというコストはスタートアップにとって大きな負担でした。Flagsmithも有力な候補でしたが、オープンソースであり、社内データを自社で管理するためにDocker経由でセルフホストできるUnleashを採用しました。最大の決め手は、UnleashのNode.js SDKがフラグ評価をメモリ(RAM)上で直接実行するため、呼び出しごとのレイテンシが0.1ms未満と極めて高速である点でした。
環境構築
システムは大きく2つのコンポーネントで構成されます。管理用UIと設定ストレージを担うUnleash Server、そしてNode.jsコード内に直接組み込むUnleash Client SDKです。
1. Docker ComposeでUnleash Serverを起動する
以下は、PostgreSQL 15データベースとともにUnleashを構築するための標準的なdocker-compose.ymlです:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash_user
POSTGRES_PASSWORD: secret_db_password
volumes:
- unleash_db_data:/var/lib/postgresql/data
restart: unless-stopped
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
DATABASE_URL: postgres://unleash_user:secret_db_password@db:5432/unleash
DATABASE_SSL: "false"
LOG_LEVEL: "warn"
INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-client-api-token"
depends_on:
- db
restart: unless-stopped
volumes:
unleash_db_data:
サービスを起動します:
docker compose up -d
約15秒後、ブラウザでhttp://localhost:4242にアクセスします。デフォルトのログイン情報は以下の通りです:
- Username:
admin - Password:
unleash4all(初回ログイン時に新しいパスワードへの変更が求められます)
2. Node.jsプロジェクトへのSDKのインストール
Expressバックエンドのディレクトリで必要なライブラリ群をインストールします:
npm install unleash-client express dotenv
設定と統合
1. UnleashでのAPIトークンの作成
Node.jsサーバーがフラグの状態を取得できるように、アクセス権限を持つトークンを作成する必要があります:
- Unleashダッシュボードにアクセスし、Settings > API accessを開きます。
- New API tokenを選択します。
- タイプとしてServer-side SDK (Client)を選択し、プロジェクトにdefault、環境にdevelopmentを割り当てます。
- 画面に表示されたトークン文字列を保存しておきます。
続いて、Feature togglesメニューに移動し、new-checkout-flowという名前の新しいフラグを作成します。
2. SingletonパターンによるUnleash Clientの初期化
UnleashのSDKはIn-memory & Background pollingメカニズムで動作します。クライアントはバックグラウンドで数秒ごとにリクエストを送信し、すべてのルールを取得してRAM上にキャッシュします。APIが判定関数を呼び出す際、ロジックはローカルメモリ上で完結して実行されます。外部への追加HTTPリクエストが発生しないため、極めて高速なレスポンスが保証されます。
src/unleash.jsを作成します:
const { initialize } = require('unleash-client');
let unleashInstance = null;
function getUnleash() {
if (!unleashInstance) {
unleashInstance = initialize({
url: process.env.UNLEASH_URL || 'http://localhost:4242/api/',
appName: 'payment-service',
environment: process.env.NODE_ENV || 'development',
customHeaders: {
Authorization: process.env.UNLEASH_API_TOKEN || '*:development.unleash-insecure-client-api-token',
},
// 開発時は5秒ごとに新しいルールを同期(本番環境では10000〜15000msを推奨)
refreshInterval: 5000,
// 30秒ごとにhit/missの統計メトリクスをサーバーへ送信
metricsInterval: 30000,
});
unleashInstance.on('ready', () => {
console.log('[Unleash] SDKがRAMへのルール読み込みに成功しました。');
});
unleashInstance.on('error', (err) => {
console.error('[Unleash] 設定の同期エラー:', err.message);
});
}
return unleashInstance;
}
module.exports = { getUnleash };
3. 決済エンドポイントへのフラグの適用
以下は、ビジネスロジックをフラグでラップする方法を示したsrc/app.jsです:
const express = require('express');
const { getUnleash } = require('./unleash');
const app = express();
app.use(express.json());
const unleash = getUnleash();
app.post('/api/checkout', (req, res) => {
const userId = req.headers['x-user-id'] || 'anonymous';
const userEmail = req.headers['x-user-email'] || '';
// Unleashがセグメント条件を評価するためのコンテキスト
const context = {
userId: userId,
properties: {
email: userEmail,
plan: req.body.plan || 'free',
},
};
// 安全なフォールバック値としてfalseを指定してフラグ状態を判定
const isNewFlowActive = unleash.isEnabled('new-checkout-flow', context, false);
if (isNewFlowActive) {
// 新しいStripe Elements決済フロー
return res.json({
version: 'v2',
gateway: 'stripe-one-click',
orderId: 'ORD-2026-NEW',
});
}
// 安定性が検証済みの従来の決済フロー
return res.json({
version: 'v1',
gateway: 'standard-redirect',
orderId: 'ORD-2026-LEGACY',
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`バックエンドサーバーがポート ${PORT} で待受中`);
});
関数unleash.isEnabled('new-checkout-flow', context, false)の第3引数に注目してください。これは安全なフォールバック値(デフォルト値)です。Unleash Serverにネットワーク障害が発生したりコンテナが再起動したりした場合でも、SDKは即座にfalseを返します。アプリケーションは例外をスローしたりプロセスをクラッシュさせたりすることなく、自動的に従来のフローへと切り替わります。
4. Activation Strategies(有効化戦略)による機能配信
Unleashは単なる全開/全閉のオン・オフにとどまりません。その真価は、UIから直接設定できる柔軟な動的配信戦略にあります:
- Gradual Rollout(カナリアテスト): ユーザーの10%、25%、そして徐々に100%へとフラグを段階的に解放できます。
userIdに基づくMurmurHash3アルゴリズムにより一貫性が保たれます。一度10%のグループに含まれたユーザーAは、次回以降のログインでも常に新しいUIを体験することになります。 - ユーザーIDターゲティング: 特定のアカウントリスト(例:
dev_quang,qa_lan)を指定し、社内チームが本番環境で先行検証できるようにします。 - Custom Constraints(カスタム制約): メールアドレスのドメインが
@company.comで終わるユーザーや、アプリバージョン2.4.0以上など、高度な条件でフィルタリングします。
テストと運用
1. 即時フロー切り替えのテスト
Expressサーバーを起動します:
node src/app.js
フラグnew-checkout-flowがOFFの状態でテストリクエストを送信します:
curl -X POST http://localhost:3000/api/checkout \
-H "Content-Type: application/json" \
-H "x-user-id: user_123"
返却されるレスポンス(v1版):
{"version":"v1","gateway":"standard-redirect","orderId":"ORD-2026-LEGACY"}
次に、UnleashダッシュボードでフラグをONに切り替えます。SDKのポーリングサイクル(5秒)が完了するのを待ってから、先ほどのcurlコマンドを再実行します。結果は即座にv2版に切り替わります:
{"version":"v2","gateway":"stripe-one-click","orderId":"ORD-2026-NEW"}
フローの切り替えプロセス全体が一瞬で完了しました。HTTP接続の切断も、コンテナの再起動(リロード)も一切発生しません。
2. Metricsタブによる効果測定
isEnabled関数が呼び出されるたびに、SDKはメトリクスを自動集計し、定期的にサーバーへ送信します。フラグ詳細を開いてMetricsタブに切り替えると、以下のデータを確認できます:
- 直近1時間における
Yes分岐とNo分岐のヒット数比較。 - 2つのコードバージョン間に分散された実際のトラフィック比率。
- 100%有効化されてから長期間経過しているにもかかわらず、ソースコードからクリーンアップされていない不要フラグの早期検知。
3. Feature Flagsを技術的負債にしないための3つの原則
1日約5万件の注文を処理するシステムで半年以上運用した結果、私たちのチームは以下の3つの貴重な教訓を得ました:
- フラグ命名規則のプレフィックス統一: 最初から目的を明確に分類します。段階的ロールアウトを行う新機能には
release-、外部連携などの障害時に緊急停止するスイッチにはkillswitch-、短期的なA/Bテストにはexperiment-を使用します。 - リリース後のフラグ完全削除: フラグはリスクを軽減するために導入されますが、長期間放置すると技術的負債(technical debt)を肥大化させます。機能が100%の状態で2週間安定稼働したら、直後のスプリントで
if/elseブロックを削除し、Unleashからフラグを破棄するチケットを即座に作成すべきです。 - フラグの独立性を最優先: フラグのネスト(入れ子構造)は極力避けてください。2〜3層にネストされたフィーチャーフラグのコードは、ユニットテストの記述や不具合の再現を悪夢へと変えてしまいます。
Unleashのおかげで、本番環境への新機能リリースはもはや「金曜夜の賭け」ではなくなりました。すべてのリスクをわずか1秒で遮断できる安心感があるからこそ、開発チーム全体が自信を持ってプロダクトのデリバリー速度を加速させることができるのです。
