Kuboyage

・ 設計

Feature Flagで実現する柔軟なリリース戦略とOpenFeatureを活用した導入コスト削減 🚀

はじめに 👋

こんにちは!H×Hのセンリツ大好きエンジニアです!✌️

皆さんは「新機能をリリースしたいけど、万が一問題が起きたらすぐに戻せるようにしたい」「一部のユーザーだけに新機能を試してもらいたい」といった要望に直面したことはありませんか?🤔

そんな悩みを解決してくれるのが Feature Flag(フィーチャーフラグ) という仕組みです!✨

この記事では、Feature Flag の基本概念から、実際の活用方法、そして OpenFeature という OSS を使った実装例まで、幅広く解説していきます。この記事はおよそ 8500 字なので、15 分で読めます!

※この記事は 7 割程度が AI によって執筆されています。

目次 📚

  1. Feature Flag とは何か
  2. Feature Flag の種類と使い分け
  3. Feature Flag のメリットとデメリット
  4. OpenFeature で実現する標準化
  5. OpenFeature の実装例
  6. 実運用での注意点とベストプラクティス
  7. まとめ

1. Feature Flag とは何か 🎯

Feature Flag の定義

Feature Flag(フィーチャーフラグ)は、コードを変更せずにシステムの振る舞いを変更できる仕組みです。

簡単に言えば、以下のような条件分岐を実行時に制御できるようにする機能です:

if (FEATURE_FLAG === true) {
  // 新しい実装
  return newImplementation();
} else {
  // 既存の実装
  return existingImplementation();
}

この FEATURE_FLAG の値を、コードをデプロイすることなく動的に変更できるのが Feature Flag の強みです!💪

なぜ Feature Flag が必要なのか

従来の開発では、新機能をリリースするには以下のような課題がありました:

  • 🚫 リリース後に問題が見つかっても、すぐに元に戻せない
  • 🚫 一部のユーザーだけに試してもらうことができない
  • 🚫 開発中の機能を main ブランチにマージできない(長寿命のフィーチャーブランチが必要)
  • 🚫 A/B テストを実施するには、複雑な仕組みが必要

Feature Flag を導入することで、これらの課題を一気に解決できます!✨


2. Feature Flag の種類と使い分け 🗂️

Feature Flag には、用途に応じていくつかの種類があります。それぞれの特徴と使い分けを見ていきましょう!

2.1 Release Toggles(リリーストグル)

開発中の機能を本番環境にデプロイしつつ、ユーザーには公開しないための仕組みです。

使用シーン

  • トランクベース開発(Trunk-Based Development)の実現
  • 開発中の機能を main ブランチにマージしながら、リリースタイミングを柔軟に制御

特徴

  • 短命であるべき:理想的には 1〜2 週間程度で削除
  • 機能が完成したら、フラグを削除してコードをクリーンに保つ
// Release Toggle の例
function renderDashboard() {
  if (featureFlags.get('new-dashboard-ui')) {
    return <NewDashboard />;
  }
  return <LegacyDashboard />;
}

2.2 Experiment Toggles(実験トグル)

A/B テストやカナリアリリースのための仕組みです。

使用シーン

  • 新機能の効果を測定する A/B テスト
  • 一部のユーザーに先行して機能を提供するカナリアリリース
  • 仮説検証のための段階的なロールアウト

特徴

  • ユーザーコンテキスト(ユーザー ID、属性など)に基づいて動的に評価
  • データ分析と組み合わせて効果測定を実施
// Experiment Toggle の例
const showNewCheckoutFlow = await featureFlags.getBooleanValue(
  'new-checkout-flow',
  false,
  { userId: currentUser.id, segment: currentUser.segment }
);

if (showNewCheckoutFlow) {
  return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;

2.3 Ops Toggles(運用トグル)

システムの運用面を制御するための仕組みです。

使用シーン

  • 高負荷時に一部の機能を無効化してシステムを保護
  • パフォーマンスに影響がある機能の ON/OFF
  • サーキットブレーカーとしての利用

特徴

  • 本番環境で動的に切り替える必要がある
  • 障害対応や負荷対策に活用
// Ops Toggle の例
async function processOrder(order: Order) {
  const enableExpensiveAnalytics = await featureFlags.getBooleanValue(
    'expensive-analytics',
    true
  );

  // 注文処理
  await saveOrder(order);

  // 高負荷時は分析処理をスキップ
  if (enableExpensiveAnalytics) {
    await runAnalytics(order);
  }
}

2.4 Permissioning Toggles(権限トグル)

特定のユーザーやテナントに機能を提供するための仕組みです。

使用シーン

  • 有料プランのユーザーのみに提供する機能
  • ベータテスターのみに公開する機能
  • エンタープライズ向けの限定機能

特徴

  • 長期間存続する可能性が高い
  • ビジネスロジックと密接に関連
// Permissioning Toggle の例
const hasAdvancedReporting = await featureFlags.getBooleanValue(
  'advanced-reporting',
  false,
  { userId: user.id, plan: user.subscriptionPlan }
);

if (hasAdvancedReporting) {
  return <AdvancedReportingDashboard />;
}
return <BasicReportingDashboard />;
Feature Flag の種類比較表
種類主な用途寿命動的性コンテキスト依存
Release Togglesトランクベース開発短命(1〜2週間)低低
Experiment TogglesA/Bテスト、カナリアリリース中期(数週間〜数ヶ月)高高
Ops Togglesシステム運用、負荷制御長期高低
Permissioning Togglesアクセス制御、プラン制限長期中高

3. Feature Flag のメリットとデメリット ⚖️

メリット 🎉

1. トランクベース開発の実現

長寿命のフィーチャーブランチを避け、常に main ブランチにマージできます。これにより:

  • マージコンフリクトの削減
  • コードレビューの効率化
  • チーム全体のコードベースへの可視性向上

2. 迅速なロールバック

問題が発生した場合、コードをデプロイし直すことなく、フラグを切り替えるだけで即座に元に戻せます。

3. 段階的なリリース

全ユーザーに一斉リリースするのではなく:

  • 1% のユーザーから始める
  • 問題がなければ 10%、50%、100% と段階的に拡大
  • リスクを最小限に抑えたリリースが可能

4. A/B テストの容易化

複数のバージョンを同時に動かし、どちらが優れているかを検証できます。

5. ビジネス主導のリリース

エンジニアがデプロイタイミングを気にせず、ビジネス側が最適なタイミングでフラグを有効化できます。

デメリット 😰

1. コードの複雑化

フラグが増えると、条件分岐が増えてコードが読みづらくなります。

// ❌ フラグが多すぎる例
if (featureFlags.get('flag1') && featureFlags.get('flag2')) {
  if (featureFlags.get('flag3')) {
    // ...
  } else if (featureFlags.get('flag4')) {
    // ...
  }
}

2. テストの複雑化

フラグの組み合わせが増えると、テストケースが爆発的に増加します。

3. パフォーマンスオーバーヘッド

フラグの評価にはコストがかかります。特に、外部サービスに問い合わせる場合は遅延が発生する可能性があります。

4. フラグの管理コスト

  • 不要になったフラグを削除し忘れると、技術的負債になります
  • フラグの使用状況を追跡する仕組みが必要

5. 予期しない動作

複数のフラグが相互作用して、予期しない動作を引き起こす可能性があります。

ベストプラクティスで対策 💡

これらのデメリットは、適切な運用ルールで軽減できます:

  • ✅ フラグの寿命を明確にする(Release Toggles は短命に)
  • ✅ 不要になったフラグは速やかに削除する
  • ✅ フラグの使用状況を追跡する仕組みを導入
  • ✅ フラグの命名規則を統一する
  • ✅ フラグのドキュメントを整備する

4. OpenFeature で実現する標準化 🌐

OpenFeature とは

OpenFeature は、Feature Flag のためのオープンスタンダードです。CNCF(Cloud Native Computing Foundation)のプロジェクトとして開発されています。

OpenFeature の目的

従来、Feature Flag を実装する際には:

  • 各社独自の実装が乱立
  • ベンダーロックインのリスク
  • プロバイダーを変更する際の大規模な書き換えが必要

これらの課題を解決するため、OpenFeature はベンダー中立的な API を提供します!✨

OpenFeature のアーキテクチャ

OpenFeature は以下のコンポーネントで構成されています:

┌─────────────────────────────────────────────┐
│         Application Code                    │
│  (Feature Flag の評価を呼び出す)            │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│         OpenFeature SDK                     │
│  (統一された API を提供)                     │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│         Provider                            │
│  (フラグ管理システムとの橋渡し)              │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│    Flag Management System                   │
│  (LaunchDarkly, flagd, Unleash, etc.)       │
└─────────────────────────────────────────────┘

主要な概念

1. Evaluation API

開発者がフラグを評価するためのインターフェースです。

const isEnabled = await client.getBooleanValue('my-feature', false);
2. Evaluation Context

フラグの評価に必要なコンテキスト情報(ユーザー ID、属性など)を渡すための仕組みです。

const context = {
  targetingKey: userId,
  email: user.email,
  plan: user.subscriptionPlan
};

const isEnabled = await client.getBooleanValue('my-feature', false, context);
3. Provider

OpenFeature SDK と実際のフラグ管理システムをつなぐアダプターです。

サポートされているプロバイダーには以下があります:

  • SaaS: LaunchDarkly, Harness, PostHog, DevCycle
  • OSS: flagd, Flipt, Unleash
  • クラウドサービス: AWS AppConfig, Azure App Configuration
4. Hooks

フラグ評価の前後に処理を挿入できる拡張ポイントです。

使用例:

  • ログの記録
  • メトリクスの送信
  • キャッシュの制御
const loggingHook = {
  before: async (context) => {
    console.log(`Evaluating flag: ${context.flagKey}`);
  },
  after: async (context, details) => {
    console.log(`Flag ${context.flagKey} evaluated to: ${details.value}`);
  }
};

client.addHooks(loggingHook);

OpenFeature のメリット

1. ベンダーロックインの回避

プロバイダーを変更しても、アプリケーションコードは変更不要です!

// プロバイダーを変更してもコードは同じ
await OpenFeature.setProvider(new LaunchDarklyProvider());
// または
await OpenFeature.setProvider(new FlagdProvider());

// アプリケーションコードは変更不要
const client = OpenFeature.getClient();
const isEnabled = await client.getBooleanValue('my-feature', false);

2. 統一された開発体験

複数の言語・フレームワークで一貫した API を使用できます。

サポートされている言語:

  • サーバーサイド: Go, Java, .NET, Node.js, Python, PHP, Ruby, Rust, Dart
  • クライアントサイド: JavaScript/TypeScript, React, Angular, iOS, Kotlin

3. コミュニティ主導の進化

オープンソースコミュニティによって、継続的に改善されています。


5. OpenFeature の実装例 💻

5.1 基本的なセットアップ(Node.js + flagd)

flagd は、OpenFeature のリファレンス実装として提供される軽量なフラグ評価エンジンです。

Step 1: 依存関係のインストール

npm install @openfeature/server-sdk @openfeature/flagd-provider

Step 2: フラグの定義(YAML)

# flags.yaml
flags:
  new-dashboard-ui:
    state: ENABLED
    variants:
      on: true
      off: false
    defaultVariant: off

  experimental-checkout:
    state: ENABLED
    variants:
      on: true
      off: false
    defaultVariant: off
    targeting:
      if:
        - in:
            - $flagd.flagKey: "experimental-checkout"
            - $user.email:
                - "[email protected]"
                - "[email protected]"
        - on
        - off

Step 3: OpenFeature の初期化

import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';

// Providerの設定
await OpenFeature.setProvider(new FlagdProvider({
  host: 'localhost',
  port: 8013,
}));

// Clientの取得
const client = OpenFeature.getClient();

Step 4: フラグの評価

// Boolean フラグ
const showNewDashboard = await client.getBooleanValue(
  'new-dashboard-ui',
  false  // デフォルト値
);

if (showNewDashboard) {
  console.log('新しいダッシュボードUIを表示');
}

// コンテキスト付きの評価
const context = {
  targetingKey: user.id,
  email: user.email,
  plan: user.subscriptionPlan
};

const showExperimentalCheckout = await client.getBooleanValue(
  'experimental-checkout',
  false,
  context
);
flagd の起動方法

flagd をローカルで起動するには、Docker を使用するのが簡単です:

# flagd を Docker で起動
docker run \
  -p 8013:8013 \
  -v $(pwd)/flags.yaml:/flags.yaml \
  ghcr.io/open-feature/flagd:latest \
  start --uri file:./flags.yaml

これで、localhost:8013 で flagd が起動し、flags.yaml で定義したフラグを評価できるようになります!

5.2 React アプリケーションでの実装

React アプリケーションでは、専用の SDK を使用して簡単に Feature Flag を統合できます。

Step 1: 依存関係のインストール

npm install @openfeature/react-sdk @openfeature/web-sdk @openfeature/flagd-web-provider

Step 2: Provider のセットアップ

// src/featureFlags.ts
import { OpenFeature } from '@openfeature/web-sdk';
import { FlagdWebProvider } from '@openfeature/flagd-web-provider';

export async function initializeFeatureFlags() {
  await OpenFeature.setProvider(
    new FlagdWebProvider({
      host: 'localhost',
      port: 8013,
    })
  );
}

Step 3: アプリケーションのラップ

// src/App.tsx
import { OpenFeatureProvider } from '@openfeature/react-sdk';
import { initializeFeatureFlags } from './featureFlags';
import { useEffect, useState } from 'react';

function App() {
  const [flagsReady, setFlagsReady] = useState(false);

  useEffect(() => {
    initializeFeatureFlags().then(() => {
      setFlagsReady(true);
    });
  }, []);

  if (!flagsReady) {
    return <div>Loading feature flags...</div>;
  }

  return (
    <OpenFeatureProvider>
      <Dashboard />
    </OpenFeatureProvider>
  );
}

Step 4: コンポーネントでフラグを使用

// src/components/Dashboard.tsx
import { useBooleanFlagValue } from '@openfeature/react-sdk';

function Dashboard() {
  const showNewUI = useBooleanFlagValue('new-dashboard-ui', false);

  if (showNewUI) {
    return <NewDashboard />;
  }

  return <LegacyDashboard />;
}

5.3 Java/Spring Boot での実装

Java アプリケーションでも OpenFeature を簡単に統合できます。

Step 1: 依存関係の追加

<!-- pom.xml -->
<dependency>
    <groupId>dev.openfeature</groupId>
    <artifactId>sdk</artifactId>
    <version>1.7.0</version>
</dependency>
<dependency>
    <groupId>dev.openfeature.contrib.providers</groupId>
    <artifactId>flagd</artifactId>
    <version>0.6.0</version>
</dependency>

Step 2: 設定クラス

import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeatureFlagConfig {

    @PostConstruct
    public void init() {
        FlagdProvider provider = new FlagdProvider();
        OpenFeatureAPI.getInstance().setProvider(provider);
    }
}

Step 3: フラグの評価

import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.Value;

@Service
public class DashboardService {

    private final Client client;

    public DashboardService() {
        this.client = OpenFeatureAPI.getInstance().getClient();
    }

    public Dashboard getDashboard(String userId) {
        // コンテキストの構築
        MutableContext context = new MutableContext();
        context.add("user_id", new Value(userId));

        // フラグの評価
        boolean showNewUI = client.getBooleanValue(
            "new-dashboard-ui",
            false,
            context
        );

        if (showNewUI) {
            return new NewDashboard();
        }
        return new LegacyDashboard();
    }
}

6. 実運用での注意点とベストプラクティス 🛠️

6.1 フラグのライフサイクル管理

フラグの削除戦略

不要になったフラグを放置すると、技術的負債になります。以下の戦略で管理しましょう:

1. 自動検出の仕組み

// 未使用フラグを検出する例
async function detectUnusedFlags() {
  const allFlags = await getAllFlagsFromConfig();
  const usedFlags = await scanCodebaseForFlagUsage();

  const unusedFlags = allFlags.filter(
    flag => !usedFlags.includes(flag)
  );

  console.log('Unused flags:', unusedFlags);
}

2. フラグに有効期限を設定

# flags.yaml
flags:
  temporary-feature:
    state: ENABLED
    variants:
      on: true
      off: false
    defaultVariant: on
    # メタデータに有効期限を記載
    metadata:
      expiresAt: "2025-12-31"
      purpose: "Black Friday campaign"

3. 定期的なクリーンアップ

週次または月次で、不要になったフラグを削除するプロセスを確立します。

6.2 命名規則

フラグの命名規則を統一することで、管理が容易になります。

// ✅ 良い命名例
'checkout-new-payment-flow'       // 機能を明確に表現
'experiment-homepage-hero-v2'     // 実験であることが明確
'ops-disable-expensive-analytics' // 運用フラグであることが明確
'premium-advanced-reporting'      // 権限フラグであることが明確

// ❌ 悪い命名例
'flag1'                          // 何のフラグか不明
'test'                           // 抽象的すぎる
'new-feature'                    // どの機能か不明

推奨される命名パターン:

<category>-<feature>-<variant>

例:
- release-checkout-redesign
- experiment-pricing-page-v2
- ops-cache-warmup
- premium-analytics-dashboard

6.3 パフォーマンス最適化

In-Process Evaluation

フラグの評価を外部サービスに問い合わせると、レイテンシが発生します。In-Process Evaluation を使用することで、ローカルでフラグを評価できます。

// flagd の In-Process モード
import { FlagdProvider } from '@openfeature/flagd-provider';

const provider = new FlagdProvider({
  // オフライン評価モード
  offline: true,
  // ローカルのフラグ定義を読み込む
  flagsFile: './flags.yaml'
});

キャッシング

頻繁に評価されるフラグは、キャッシュすることでパフォーマンスを向上できます。

const flagCache = new Map<string, boolean>();

async function getCachedFlagValue(
  flagKey: string,
  defaultValue: boolean,
  ttlMs: number = 60000
): Promise<boolean> {
  const cached = flagCache.get(flagKey);
  if (cached !== undefined) {
    return cached;
  }

  const value = await client.getBooleanValue(flagKey, defaultValue);
  flagCache.set(flagKey, value);

  // TTL後にキャッシュをクリア
  setTimeout(() => flagCache.delete(flagKey), ttlMs);

  return value;
}

6.4 モニタリングと可観測性

OpenTelemetry との統合

OpenFeature は OpenTelemetry と統合できます。

import { trace } from '@opentelemetry/api';

const tracingHook = {
  before: async (context) => {
    const span = trace.getTracer('feature-flags').startSpan('evaluate-flag');
    span.setAttribute('flag.key', context.flagKey);
  },
  after: async (context, details) => {
    const span = trace.getActiveSpan();
    span?.setAttribute('flag.value', details.value);
    span?.setAttribute('flag.variant', details.variant);
    span?.end();
  },
  error: async (context, error) => {
    const span = trace.getActiveSpan();
    span?.recordException(error);
    span?.end();
  }
};

client.addHooks(tracingHook);

メトリクスの収集

const metricsHook = {
  after: async (context, details) => {
    // Prometheusなどのメトリクスシステムに送信
    flagEvaluationCounter.inc({
      flag_key: context.flagKey,
      variant: details.variant,
      reason: details.reason
    });
  }
};

client.addHooks(metricsHook);

6.5 セキュリティ

機密情報の扱い

フラグのメタデータに機密情報を含めないように注意しましょう。

# ❌ 悪い例:機密情報を含む
flags:
  premium-feature:
    metadata:
      apiKey: "secret-key-12345"  # 機密情報を含めない!

# ✅ 良い例:環境変数を参照
flags:
  premium-feature:
    metadata:
      apiKeyRef: "PREMIUM_API_KEY"  # 環境変数名を参照

アクセス制御

本番環境のフラグを変更できる権限は、適切に制限しましょう。


7. まとめ 🎊

Feature Flag を導入するべき理由

Feature Flag は、モダンな開発において以下のメリットをもたらします:

  • 安全なリリース: 問題が起きても即座にロールバック可能
  • 柔軟なデプロイ: コードのデプロイとリリースタイミングを分離
  • 段階的なロールアウト: リスクを最小限に抑えた展開
  • A/B テストの実現: データに基づいた意思決定
  • トランクベース開発: 長寿命のフィーチャーブランチからの解放

OpenFeature を選ぶ理由

OpenFeature は、Feature Flag の標準化を実現します:

  • ベンダーロックインの回避: プロバイダーを自由に変更可能
  • 統一された開発体験: 複数の言語・環境で一貫した API
  • 豊富なエコシステム: 多数のプロバイダーとの統合
  • オープンソース: コミュニティ主導の透明な開発

始め方

Feature Flag を導入する際は、以下のステップで始めましょう:

  1. 小さく始める: まずは 1〜2 つのフラグから試す
  2. Release Toggles から: 短命なフラグで運用に慣れる
  3. 削除プロセスを確立: 不要になったフラグを速やかに削除する仕組み
  4. チーム全体で理解を共有: Feature Flag の目的と使い方を浸透させる
  5. 段階的に拡大: 成功体験を積み重ねながら、適用範囲を広げる

参考リンク 🔗

Feature Flag は、開発チームに大きな柔軟性をもたらす強力なツールです。適切に活用することで、より安全で迅速なリリースを実現できます!🚀

ぜひ、皆さんのプロジェクトでも Feature Flag を導入してみてください!✨

ブログの記事一覧へ