🌊

OpenFeatureとflagdで構築するモダンなフィーチャーフラグシステム

に公開

OpenFeatureとflagdで構築するモダンなフィーチャーフラグシステム

はじめに

フィーチャーフラグ(Feature Flag)は、アプリケーションの機能をデプロイ後にリアルタイムで制御できる仕組みです。新機能の段階的リリース、A/Bテスト、緊急時の機能無効化など、現代のソフトウェア開発には欠かせない技術となっています。

本記事では、オープンソースのOpenFeatureとflagdを使用したフィーチャーフラグシステムの実装について、実際のJavaプロジェクトのコードベースを基に詳しく解説します。Spring Bootアプリケーションでの実装例から、Dockerを使った環境構築、監視システムの統合まで、実践的な内容をカバーします。

フィーチャーフラグとは?

基本概念

フィーチャーフラグは、コードの実行パスを動的に制御する仕組みです:

@GetMapping("/users")
public List<User> getUsers() {
    // フィーチャーフラグで新しいAPIバージョンを制御
    String apiVersion = featureflagService.getStringFlag("userApiVersion");
    
    if ("v2".equals(apiVersion)) {
        return userServiceV2.getAllUsers(); // 新しい実装
    } else {
        return userServiceV1.getAllUsers(); // 従来の実装
    }
}

フィーチャーフラグの特徴

フィーチャーフラグは、遠隔設定とは異なり、条件に基づいた動的な機能制御を行います:

  • 条件付き評価: ユーザー属性、環境、時間などに基づいて異なる値を返す
  • 段階的ロールアウト: 特定の割合のユーザーにのみ新機能を提供
  • A/Bテスト: 複数のバリエーションを同時にテスト
  • 緊急停止: 問題発生時に即座に機能を無効化
// フィーチャーフラグの例:条件に基づく動的制御
EvaluationContext context = EvaluationContext.builder()
    .add("userId", userId)
    .add("userTier", "premium")
    .build();

boolean useNewFeature = featureflagService.getBooleanFlag("newPaymentEngine", context);

OpenFeature + flagdアーキテクチャ

技術スタック

  • OpenFeature: CNCF標準のフィーチャーフラグAPI
  • flagd: OpenFeatureのリファレンス実装エンジン
  • Spring Boot: Javaアプリケーションフレームワーク
  • Docker Compose: コンテナオーケストレーション
  • Prometheus + Grafana: 監視・メトリクス収集

システム構成

# docker-compose.yaml
version: '3'
services:
  featureflag-service:
    image: featureflag-app:0.0.1-SNAPSHOT
    networks:
      - openfeature_network
    depends_on:
      - featureflag-engine
    ports:
      - "8080:8080"
    environment:
      - FEATUREFLAG_ENGINE_HOST=featureflag-engine
      - FEATUREFLAG_ENGINE_PORT=8013
      - CACHE_TYPE=lru  # キャッシュタイプ設定
      - MAX_CACHE_SIZE=10000  # キャッシュサイズ設定

  featureflag-engine:
    image: ghcr.io/open-feature/flagd:latest
    command: start --uri https://raw.githubusercontent.com/KimByeongKou/PublicJsonFiles/main/example_flags.json
    networks:
      - openfeature_network
    ports:
      - "8013:8013"
    volumes:
      - ./flagd:/etc/flagd

networks:
  openfeature_network:
    driver: bridge

flagdの設定ファイル構造

flagdはフィーチャーフラグの設定をJSONファイルで管理します。実際のプロジェクトで使用されている設定例:

{
  "flags": {
    "myStringFlag": {
      "state": "ENABLED",
      "variants": {
        "key1": "version1"
      },
      "defaultVariant": "key1"
    },
    "userApiVersion": {
      "state": "ENABLED",
      "variants": {
        "v1": "version1",
        "v2": "version2"
      },
      "defaultVariant": "v1",
      "targeting": {
        "if": [
          {
            "in": ["@user.tier", ["premium", "enterprise"]]
          },
          "v2",
          "v1"
        ]
      }
    }
  }
}

設定ファイルの要素解説

  • state: フラグの状態(ENABLED/DISABLED
  • variants: 可能な値のバリエーション定義
  • defaultVariant: デフォルトで返されるバリアント
  • targeting: 条件分岐ロジック(JSONLogic形式)

フラグ値の取得方法

1. OpenFeatureクライアントの設定

// FeatureflagAdapterConfig.java
@Configuration
@RequiredArgsConstructor
public class FeatureflagAdapterConfig {
    private final Environment environment;
    private final MeterRegistry registry;

    @Bean
    public Client featureflagAdapter() {
        // 環境変数からflagd接続設定を取得
        String flagdHost = environment.getProperty("FEATUREFLAG_ENGINE_HOST", "localhost");
        String flagdPort = environment.getProperty("FEATUREFLAG_ENGINE_PORT", "8013");
        String maxCacheSize = environment.getProperty("MAX_CACHE_SIZE", "100");
        String cacheType = environment.getProperty("CACHE_TYPE", "lru");
        
        // FlagdProviderの設定
        FlagdProvider flagd = new FlagdProvider(
            FlagdOptions.builder()
                .resolverType(Config.Evaluator.RPC)  // gRPC通信を使用
                .host(flagdHost)
                .port(Integer.parseInt(flagdPort))
                .tls(false)  // TLS無効(開発環境用)
                .maxCacheSize(Integer.parseInt(maxCacheSize))
                .retryBackoffMs(100)  // リトライ間隔
                .deadline(3000)  // タイムアウト設定
                .cacheType(cacheType)  // キャッシュタイプ(lru/custom)
                .build(),
            featureflagCounter()  // メトリクス収集
        );
        
        // OpenFeature APIにプロバイダーを設定
        OpenFeatureAPI api = OpenFeatureAPI.getInstance();
        api.setProviderAndWait(flagd);
        return api.getClient();
    }
}

2. フラグ値取得の実装パターン

// 基本的な値取得
public String getStringFlag(String key) {
    return flagdClient.getStringValue(key, "default");
}

// 条件付き値取得
public String getStringFlagWithContext(String key, String userId, String userTier) {
    EvaluationContext context = EvaluationContext.builder()
        .add("userId", userId)
        .add("user.tier", userTier)  // targetingで使用されるキー
        .build();
    
    return flagdClient.getStringValue(key, "default", context);
}

// ブール値取得
public boolean getBooleanFlag(String key, EvaluationContext context) {
    return flagdClient.getBooleanValue(key, false, context);
}

// 数値取得
public int getIntegerFlag(String key) {
    return flagdClient.getIntegerValue(key, 0);
}

Clean Architectureでの実装

// 1. Controller層
@RestController
@RequestMapping("/api/v1/resolve")
public class FeatureflagResolveControllerV1 {
    private final FeatureflagService featureflagService;
    
    @GetMapping("/string/{key}")
    public String getStringFlagValue(@PathVariable String key) {
        try {
            return featureflagService.getStringFlag(key);
        } catch (Exception e) {
            return ""; // 安全なフォールバック
        }
    }
}

// 2. Service層
@Service
public class FeatureflagService {
    private final GetStringValuePort getStringValuePort;
    
    public String getStringFlag(String key) throws Exception {
        return getStringValuePort.getStringValue(
            new GetStringValuePortRequest(key)
        );
    }
}

// 3. Adapter層
@Component
public class FlagdAdapter implements GetStringValuePort {
    private final Client flagdClient; // OpenFeature Client
    
    @Override
    public String getStringValue(GetStringValuePortRequest request) {
        return flagdClient.getStringValue(request.getKey(), "null");
    }
}

サーバーサイド vs クライアントサイド

サーバーサイド決定(推奨)

@GetMapping("/payment")
public PaymentResponse processPayment(@RequestParam String userId) {
    // サーバーがユーザー情報を基にフラグを評価
    EvaluationContext context = EvaluationContext.builder()
        .add("userId", userId)
        .add("userTier", getUserTier(userId))
        .build();
    
    boolean useNewEngine = featureflagService
        .getBooleanFlag("newPaymentEngine", context);
    
    return useNewEngine ? 
        newPaymentService.process(userId) : 
        legacyPaymentService.process(userId);
}

メリット:

  • セキュリティ:クライアントが設定を改ざんできない
  • 一貫性:全クライアントで同じロジック
  • 複雑な条件:ユーザー属性、時間、地域などを評価可能

実際の使用例とベストプラクティス

フィーチャーフラグの実践的な活用例

1. 新機能の段階的ロールアウト

@GetMapping("/api/users")
public ResponseEntity<List<User>> getUsers(@RequestParam String userId) {
    EvaluationContext context = EvaluationContext.builder()
        .add("userId", userId)
        .add("userTier", getUserTier(userId))
        .build();
    
    String apiVersion = featureflagService.getStringFlag("userApiVersion", context);
    
    if ("v2".equals(apiVersion)) {
        return ResponseEntity.ok(userServiceV2.getAllUsers());
    } else {
        return ResponseEntity.ok(userServiceV1.getAllUsers());
    }
}

2. A/Bテストの実装

@GetMapping("/payment")
public PaymentResponse processPayment(@RequestParam String userId) {
    EvaluationContext context = EvaluationContext.builder()
        .add("userId", userId)
        .build();
    
    boolean useNewEngine = featureflagService.getBooleanFlag("newPaymentEngine", context);
    
    if (useNewEngine) {
        return newPaymentService.process(userId);
    } else {
        return legacyPaymentService.process(userId);
    }
}

運用時の監視ポイント

  • キャッシュヒット率: 90%以上を目標
  • フラグ評価時間: 平均10ms以下
  • flagd接続状態: 常時監視
  • エラー率: 1%以下を維持

まとめ

OpenFeatureとflagdを使用することで:

  • 標準化: CNCF標準に準拠したベンダー中立的な実装
  • 拡張性: 他のプロバイダーへの簡単な切り替えが可能
  • コスト効率: 完全オープンソースで無料利用
  • 本格運用: 企業レベルでの使用にも十分対応
  • 開発効率: Spring Bootとの統合で迅速な開発が可能

フィーチャーフラグは、安全で迅速なソフトウェアデリバリーを実現する重要な技術です。本記事で紹介したJavaプロジェクトの実装例を参考に、OpenFeatureエコシステムを活用してモダンな開発プロセスを構築してみてください。

参考リンク

Discussion