BASE ADVENT CALENDAR 2025 DAY.20 はじめに この記事はBASE アドベントカレンダー 2025の20日目の記事です。 Pay ID Platform Group の 大木です。 本記事では、Feature Flag(aka Feature Toggles)の標準化仕様及びSDKである OpenFeature と、Feature Flag As A Service(以下FFaaS)である AWS AppConfig を利用したサービスを約1年間運用してきたため、OpenFeatureを中心にFeature Flagの現在とAppConfigの運用に関してをお話しします。システムの開発言語はGoを利用しているため、主にそれをベースにお話しします。 Feature Flag概要 Feature Flagと聞いて何を思い浮かべるでしょうか? On/Offのスイッチのようなもので、段階的機能公開を行うカナリアリリースで利用したり、問題があった場合にロールバックできる A/B テストで、ユーザセグメントごとに異なる機能やUIを動的に振り分けたりするなどに使う コード差分を頻繁にmainブランチへマージするコードのフレッシュさを保つトランクベース開発 具体的なケースを思い浮かべると、複数の機能を持っているように思えますが 、簡単に言えば、 Feature Flagとは、コードを変更することなくシステムの動作を変更できる手法 で、システムへのコード反映(デプロイ)と機能公開(リリース)を分離することができるものです。 その活用方法として、大きく4つのカテゴリに分類することができ、設計上の制約、管理方法がそれぞれ異なります。カテゴリの分類目安として、フラグをどのくらいの期間利用するかという存続期間と、フラグの評価ロジックや値の決定をどの程度動的に行うかによって分類できるようです。 Release Flag: 開発中の未完成な機能を本番環境から隠すための、比較的短命なフラグ。 Ops Flag: システムの運用担当者が、パフォーマンスの最適化や機能の緊急停止のために使用するフラグ。比較的長命になることがあります。 Experiment Flag: A/Bテストなど、ユーザーの振る舞いを比較するために使用するフラグ。実験期間中のみ存在します。 Permission Flag: 特定のユーザーグループに機能へのアクセス権を与えるための、永続的または非常に長命なフラグ。 引用: https://martinfowler.com/articles/feature-toggles.html 我々は主に、中長期運用するOps FlagやPermission Flagに関して、OpenFeature SDKとAppConfigを利用した運用管理を行なっており、OpenFeatureの主要機能や予備知識を交えながら説明したいと思います。 OpenFeatureについて OpenFeature とは、コミュニティ主導のFeature Flag標準化プロジェクトですが、標準化仕様の策定とSDKを提供しています。LaunchDarklyやAppConfig等フラグ管理を行うベンダーが提供するAPIや独自SDKを直接利用するのではなく、OpenFeatureを利用する利点はどのようなところにあるのでしょうか? 例えば、以下のような点が挙げられると思います。 どのフラグ管理バックエンドを選択しても、アプリケーションからは、OpenFeature SDKの評価APIにより、統一的なインタフェースでフラグ評価ロジックを実装可能 FFaaSやDB、ローカルファイルなど、フラグ管理するバックエンドは、それぞれに対応するOpenFeature Providerを利用することで、アプリケーション側のフラグ評価ロジックを、ほぼ変更せずに差し替えが可能 Hooksを利用し、フラグ動的評価を行うための評価コンテキスト(Evaluate Context)を編集したり、ログ出力、Telemetryの実施など機能拡張を行うことが可能 以下の図は、OpenFeatureが、任意の仮想的なフラグ管理システム「Flags-R-us」と統合されることを示しています。あたかもクラウド上のシステムと統合されそうな印象を受けますが、3rd partyベンダーが提供する独自SDKや、DB、ローカルファイルなどフラグ管理が行える仕組みと、それに対応するProviderを用意できれば、共有の標準化されたフラグクライアント(SDK)を利用し、一貫性のある統合APIを利用して実装することが可能です。 引用: https://openfeature.dev/docs/reference/intro アプリケーションで、OpenFeature SDKを利用して、Feature FlagをGoで扱うには以下のような実装となります。もしフラグ管理バックエンドを差し替えたい場合は、1.でフラグ管理バックエンドに対応するProviderに差し替えれば、フラグ評価や利用する箇所の修正は不要です。 package main import ( "fmt" "context" "github.com/open-feature/go-sdk/openfeature" ) func main() { // 1. フラグ管理バックエンドを扱うProviderを登録 openfeature.SetProviderAndWait(openfeature.NoopProvider{}) // 2. そのProviderを、アプリケーションから利用するためのクライアントを作成 client := openfeature.NewClient( "app" ) // 3. フラグの評価実行 v2Enabled := client.Boolean( context.TODO(), "v2_enabled" , true , openfeature.EvaluationContext{}, ) // 4. 評価されたフラグ値を使う if v2Enabled { fmt.Println( "v2 is enabled" ) } } これで終わるなら、直接フラグ管理システムが提供するAPIやSDKを使えば良いかと思いますが、他にもたくさん仕様が考えられていますので、いくつか紹介します。 1. 共通のI/Fで実装したいが、アプリ制御とA/Bテストは、別々のフラグ管理システムを利用したい A/Bテストを行う際にもFeature Flagを利用することができます。我々はAWSのAppConfigをフラグ管理システムとして利用していますが、A/Bテストでは、分析基盤が用意されているかどうかがとても重要です。現在、 Amazon CloudWatch Evidently というモニタリングや分析を行える機能はサポートを終了しており、分析基盤を別途用意するのも大変です。そういった場合、以下のように複数Providerを登録することが可能です。それぞれのProviderに対応するクライアントを作成することで複数のフラグ管理システムを、用途ごとに使い分けることができます。 import "github.com/open-feature/go-sdk/openfeature" // アプリ制御用Providerを登録 openfeature.SetProviderAndWait(NewLocalProvider()) // 名前付きでProviderを登録 openfeature.SetNamedProvider( "abtesting" , NewABProvider()) // デフォルトのProviderを利用するクライアントを作成 clientWithDefault := openfeature.NewDefaultClient() // 名前付きで登録したProviderを利用するクライアントを作成 clientForABTesting := openfeature.NewClient( "abtesting" ) 2. フラグ管理システムを別システムに移行したい 例えば、サービス終了で別システムに移行しなければならないというケースを考えてみましょう。 実験的機能となりますが、 Multi-Provider というものを利用すれば、複数のProviderをまとめて登録し、一つのクライアントを使い並行して実行できます。これにより、先に新システムに対応するProviderを追加登録しておいて、後からゆっくりと新しいシステムにフラグを登録するといったことが可能になります。 import ( "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/multi" "github.com/open-feature/go-sdk/openfeature/memprovider" ) mprovider, err := multi.NewProvider( multi.StrategyFirstMatch, multi.WithProvider( "providerA" , memprovider.NewInMemoryProvider( /*...*/ )), multi.WithProvider( "providerB" , myCustomProvider), ) if err != nil { return err } openfeature.SetNamedProviderAndWait( "multiprovider" , mprovider) 3. 複数フラグ管理システムを組み合わせてシームレスに利用したい 2.のケースと同じように複数のProviderを利用するケースなのですが、組み合わせて使うケースを考えてみましょう。FFaaSを利用する場合、ネットワーク越しにフラグデータセットを取得してこなければならないため、システムがダウンしている場合、アクセスできないことがあります。そこで、バックアップとして、環境変数やローカルファイルを利用するProviderを併せて登録しておけば、その間は、フォールバックしてフラグを利用するといったことが可能になります。以下の中からProvider利用戦略を指定することで、柔軟に結果を利用することができます。 First Match: Providerを順番に呼び出し、最初にフラグが存在するものを返す First Success: Providerを順番に呼び出し、エラーが発生しなかったProviderの結果を返す Comparison: 並列にProviderを呼び出し、結果を比較し一致した場合はその結果を、そうでない場合はFallback Providerの結果またはデフォルト値を返す Custom: 自前で利用ロジックを実装 OpenFeatureでは、ターゲティングと呼ばれる仕組みがあり、アプリケーションやユーザーに関する情報を利用して、動的に評価するルール設定を定義し、フラグのステータスを制御します。 AppConfigの場合、 マルチバリアントフラグ というものを利用する必要があります。その場合、AppConfigからAPIで直接取得する方式は利用できず、AppConfig AgentをSidecarコンテナで立てて利用するのが必須となっています。AgentがAppConfigから、設定値やターゲーティングに利用するルールセットをポーリングにより定期的にダウンロードするため、アプリケーションからは、ローカルホストへの通信により、Agentでフラグ評価を実行できます。 しかし、 Agentのイメージ が壊れた場合、正しくフラグを取得できなかったり、リソース枯渇や過負荷になった場合、Agentへの接続エラーになることが極まれに発生します。そうした場合、バックアップとして、ローカル設定からフラグの結果を返せるProviderを併せて登録しておくことで、問題が起きにくくなるかなと思います。 引用: https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-agent.html 4. ターゲティングをサポートする評価コンテキストを柔軟に構築できる OpenFeatureは、ターゲティングを行う入力値として、評価コンテキストというコンテナを利用します。ここに、ユーザのIPやメールアドレス、サーバの位置情報(AWS Regionなど)を載せてリクエストすることで、柔軟にフラグ評価を行えます。 フラグ管理システムがフラグを動的に評価し結果を返す仕組みを提供していればの話ですが、それによって、柔軟に結果を変化させることができます。これはグローバルに設定したり、特定クライアント、実行時にも設定できます。様々なフェーズで設定されたコンテキストのデータはマージされて、評価リクエストに乗ります。 これを使った実装として、カートのCheckout時とPay IDアプリの非Checkout時にリクエストする同じHTTP APIがあり、そこでこのターゲティングを利用したフラグを利用しています。Permission Flagの一種の使い方ですね。 Checkout時には、前段で独自に審査ロジックが動作しているため、無駄に審査や料金が発生しないように審査ロジックをスキップするが、Pay IDアプリの非Checkout時には何も審査していないため、APIで必ず審査ロジックを実行する必要があります。その制御にこの仕組みを使っています。 // OpenFeature全体で利用する評価コンテキストを設定 openfeature.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map [ string ]any{ "region" : "us-east-1-iah-1a" , }, )) // クライアントに設定(これによって、利用するProviderのみに伝搬される) client := openfeature.NewClient( "my-app" ) client.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map [ string ]any{ "version" : "1.4.6" , }, )) // 実行時に評価コンテキストを設定 evalCtx := openfeature.NewEvaluationContext( "user-123" , map [ string ]any{ "company" : "Initech" , }, ) boolValue, err := client.BooleanValue( "boolFlag" , false , evalCtx) また、開発言語によっては、トランザクションコンテキストを利用できる場合があり、リクエストスコープなデータをそこに載せて伝搬することが可能です。 goの場合は、 context packag e を使って実現しています。これによって、HTTPリクエスト受信時にミドルウェアでIPやユーザIDを得られたら、とりあえずトランザクションコンテキストを利用し、評価コンテキストにマージするといった使い方が可能です。 import "github.com/open-feature/go-sdk/openfeature" // set the TransactionContext ctx := openfeature.WithTransactionContext(context.TODO(), openfeature.EvaluationContext{}) // get the TransactionContext from a context ec := openfeature.TransactionContext(ctx) // merge an EvaluationContext with the existing TransactionContext, preferring // the context that is passed to MergeTransactionContext tCtx := openfeature.MergeTransactionContext(ctx, openfeature.EvaluationContext{}) // use TransactionContext in a flag evaluation client.BooleanValue(tCtx, ....) 5. Hooksで機能拡張できる Hooksは、アプリケーション開発者が、フラグ評価に任意の動作を追加できる機能です。 以下の4つのステージでロジックを追加できます。 before: フラグ評価の直前 after: フラグ評価が成功した直後 error: フラグ評価が失敗した直後 finally: フラグ評価後に無条件に これは、評価コンテキストと同様、グローバルや特定クライアント毎や、評価実行時のいずれかで実行するように設定可能です。 ユースケースとしては、評価コンテキストへの編集や追加、評価後のフラグ値の検証、 Telemetryデータ計測 、ログ記録に利用できます。 OpenFeature Hooks LifeCycle 評価コンテキストへの編集に利用するフックの例としては、以下のようなものです。 AppConfigの場合、Contextはリクエストヘッダで送信します。提供されているProviderで、評価リクエストのフォーマットに合うように大体は変換されると思いますが、一部データ型の変換が、評価ルールが想定しているデータ型に合わないケースが、発生することがあります。 変換ロジックを、アプリケーション側のコードで実装することも可能ですが、フラグ管理システムを差し替えた場合、フォーマットが一致しない可能性があるため、その部分を書き直す必要が生じるかもしれません。フックを実装しOpenFeature SDKに登録すれば、アプリケーションロジックに影響を与えず、データ変換ロジックを実行することが可能となります。 package featureflag import ( "context" "maps" "strings" "time" "github.com/open-feature/go-sdk/openfeature" ) // AppConfigのContextに合うように変換するHookを実装します。 type AlterAppConfigContextHook struct { openfeature.UnimplementedHook } // AppConfigでは、HTTP Header: Contextに複数値を詰め込むため、カンマ区切りはそれらの値を分けるために使用されます。 // そのため、カンマ区切りの値を持つ配列値等の場合は、別の区切り文字に置き換える必要があります。 // そもそも配列もサポートしていなそうなため、スペース区切りに変換するようにします。 // // See https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-multi-variant-feature-flags-rules-operators.html // // Example: // // HTTP Header: Context: "targetingKey=1qaz2wsx3edc,machineId=1qaz2wsx3edc,region=us-east-1-iah-1a,tenant=e1,service=payid-api,scope=payid-app account,accessDate=2025-03-31T04:00:16" func (h *AlterAppConfigContextHook) Before(ctx context.Context, hookContext openfeature.HookContext, hookHints openfeature.HookHints) (*openfeature.EvaluationContext, error ) { oldECtx := hookContext.EvaluationContext() newAttrs := map [ string ]any{} maps.Copy(newAttrs, oldECtx.Attributes()) // 記載されている評価ルールとして利用可能なオペランドのフォーマットに合わせる for k, v := range maps.All(newAttrs) { if t, ok := v.([] string ); ok { newAttrs[k] = strings.Join(t, " " ) } if t, ok := v.(time.Time); ok { newAttrs[k] = t.Format(time.RFC3339) } } newECtx := openfeature.NewTargetlessEvaluationContext(newAttrs) return &newECtx, nil } func NewAlterAppConfigContextHook() *AlterAppConfigContextHook { return &AlterAppConfigContextHook{} } サーバサイドにおけるFeature Flag評価と取得に関する課題 これまで、フラグ管理システムがどこにあるかを意識せずに話していました。しかし、FFaasの場合、クラウド上のどこかに存在するため、どこかの段階でフラグデータセットをネットワーク越しに通信して、取得する必要があります。 その際に考えることはいくつかあるかと思います。フラグが必要になるたびに、リモートに通信してその都度評価してもらうのか、データセットを取得しローカルで評価するのかがありそうです。 Different approaches for server-side SDK architectures という記事によると、サーバーサイドSDKが一般的に採用しているアーキテクチャ上のアプローチは3つあるといいます。 Different approaches for server-side SDK architecture アーキテクチャ 仕組み 利点 欠点 "Direct" API Bridge フラグ評価のたびに、SDKがフラグ管理サービスのAPI(REST/gRPC)を直接呼び出す。 SDKの実装がシンプル。評価ロジックをサービス側に集約できる。 ネットワークのオーバーヘッドが大きく、パフォーマンスが低い。ネットワーク障害の影響を受けやすい。 API with Cache APIからのレスポンスをSDKがキャッシュし、後続の同じリクエストにはキャッシュから応答する。 ネットワークトラフィックを削減できる。 初回リクエストは遅い。キャッシュされていない動的な評価には不向き。キャッシュの更新ラグがある。 Local Evaluation SDKがフラグの全設定データをローカルに保持し、評価をメモリ上で完結させる。設定の更新はストリーミング(SSE, WebSockets)や定期的なポーリングで行う。 非常に高速でネットワークの影響を受けない。耐障害性が高い。 SDKの実装が複雑。全設定データを保持するため、メモリ使用量が増加する可能性がある。 これらのうち、Local Evaluationが最も高速かつ効率的で耐障害性に優れています。しかし、フラグ管理システムが、単純にAPIしか提供していない場合、OpenFeature Providerでこれらの仕組みを頑張って実装する必要があります。 また、独自SDKを提供している場合、そのSDKに対して呼び出しを行うOpenFeature Providerを利用もしくは作成すれば、実装コストを下げることが可能です。 さて、AppConfigの場合、どうなるのでしょうか? 先ほどお見せしたAppConfigの図を再掲します。 引用: https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-agent.html Agentとアプリケーションを一つのシステムとするなら、Local Evaluationと言えそうです。ただし、1の部分でローカルホスト宛に通信が発生しており、この通信も過負荷時、極まれに失敗することがあります。また、AppConfigでターゲティングを行う場合、このアーキテクチャは変更できません。 このAppConfig Agentを利用するProviderを実装する場合、最も単純な仕組みとするなら、対Agent向けに通信を行い、そちらでフラグ評価を実行する"Direct" API Bridge のアプローチとなります。我々は、OpenFeature Providerとして、 https://github.com/Arthur1/openfeature-provider-go-aws-appconfig を利用しています。 AppConfigにフラグを追加更新、デプロイするには? AppConfigでは、マネージメントコンソールで、直接設定データを作成し、デプロイを実行するケースのほか、 https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-type-reference-feature-flags.html のJSONスキーマに基づいた設定データを作成し、AWS SDKやCLIを使ってフラグデータセットの更新とデプロイによる設定反映が行えます。デプロイの際、デプロイ戦略を使うことにより、段階的リリースも行うことが可能です。データセットの定義は、コードベースでGit管理しており、CI/CDにより、AWS環境に反映しています。 以下の図の通り、GitHub Actionsでマージ時に差分を検知し、データセットをS3にアップロードすると、Step functionsの各ステップでLambdaが実行され、AppConfigへの設定反映とデプロイが実行されます。 このワークフローでは、定義済みデプロイ戦略: AppConfig.AllAtOnceと同様の戦略を使用しているため、段階的ではなくデプロイ完了後即時リリースとなります。また、手動トリガーでのワークフローも用意しています。 appconfig workflow フラグデータセットについて コードベースにある設定ファイルの形式は、JSONではなくYAMLにしています。ダブルクォートのエスケープなど色々考慮しないといけないためです。 AWS.AppConfig.FeatureFlags のJSONスキーマに準拠したデータ構造で、YAMLファイルに記述して管理しています。これをAppConfigへ反映する際には、JSONに変換します。 version: "1" flags: sampleflag: description: 説明 attributes: attribute_name: description: 属性説明 constraints: type : number minimum: 1 values: sampleflag: enabled: false # または _variants また、ターゲティングでマルチバリアントフラグを利用するためには、以下のように _variants にそれぞれの評価ルールを定義したデータセットを定義する必要があります。 OpenFeatureの評価コンテキストに格納された値を利用し、ruleに一致するかを判定します。以下の設定例だと、評価コンテキストに、 environment=dev と格納された場合は、一番目が選択されます。 values: sign: _variants: - name: High cache enabled: true rule: (eq $environment "dev" ) attributeValues: cache_interval: 3600 expires_in: 3600 - name: default enabled: true attributeValues: cache_interval: 300 expires_in: 3600 attributeValues を併せて定義することにより、そのフラグに関連づけられる属性を追加することができます。OpenFeatureでは、それをMetadataとして取得することが可能です。 AppConfigのフラグ属性は、Boolean形式のFeatureFlagの場合、enableの時のみ利用でき、またデータセット定義で、 _variants を使った場合でないと利用できません。 それにより、アプリケーションに様々な特性を付与することもできます。 上記のデータセット例は、AWS KMSを利用した署名リクエストを制御するフラグを想定しているのですが、毎回署名するのはコストがかかるため、署名をキャッシュしたり有効期限を設定するのに利用したりしています。 ちなみに、OpenFeature Go SDKだと、Metadataは型 map[string]any となっており、フラグ管理システムの定義または、Providerのパース処理によっては、型情報が失われてしまいます。 以下のように、ヘルパー関数を実装するのが良いでしょう。 func GetInt(metadata openfeature.FlagMetadata, key string ) (result int64 , err error ) { // map[string]any なので、int64, float64 の両方を試す intVal, err := metadata.GetInt(key) if err == nil { result = intVal return } floatVal, err := metadata.GetFloat(key) if err == nil { result = int64 (floatVal) return } return } func GetStringArray(metadata openfeature.FlagMetadata, key string ) ([] string , error ) { v, ok := metadata[key] if !ok { return nil , fmt.Errorf( "key %s does not exist in FlagMetadata" , key) } s, ok := v.([]any) if !ok { return nil , fmt.Errorf( "key %s is not an array" , key) } var strArr [] string for _, v := range s { if str, ok := v.( string ); ok { strArr = append (strArr, str) } } return strArr, nil } 注意点 FeatureFlagやFFaaSを利用すると、色々いいことづくめのように見えますが、必ずしも万能ではありません。 1. 後方互換性を保てないリリースをした場合、問題が見つかった後にさっと前の状態に戻すのは難しい コードベースをできるだけ最新に保つトランクベース開発には便利ですが、時には、後方互換性を損なう機能をリリースしないといけない場合があります。 問題が見つかった場合、フラグの切り替えで、コードパスの切り替えはできるかもしれませんが、データ構造が変わってしまった場合、緊急メンテナンスを実施し、データの更新が必要になるかもしれません。 そのため、時にはFeatureブランチで、mainブランチとは長期間切り離した状態で、開発した方が安全な可能性はあります。ただし、その戦略をとった場合、合流時にいわゆるビックバンリリースとなり、作業は大変になるかと思います。 2. 不特定多数のターゲティングには向いているが、特定ユーザの認可の代わりにはならない 認可は、ユーザーが特定のアクションを実行したり、リソースにアクセスしたりする 権限があるかどうか を検証するセキュリティメカニズムのため、目的が異なります。 何でもかんでも、FeatureFlagで制御するのはよろしくない です。 3. 不要になったRelease Flagのお掃除が面倒 リリース後安定運用でき、ロールバックしないとわかった場合、そのフラグは不要となります。ただ、FFaaSの設定を気軽に触りたくないし、そのままにした場合、もうフラグ設定を参照する必要がないのに、不要なアクセスで課金されてしまいます。 削除するにしても、併せてコードも書き換える必要があります。フラグを使わずに済むのならば、その方がリリース後に修正が不要なため、簡潔に実装できます。また、リリース後にも、無駄に課金されることを考えると、場合によっては、環境変数やローカル設定経由の定義で済ませた方が良いということもあります。 一方で、高負荷時やアラートをトリガーに機能オフにするキルスイッチとして、リリース後にもOps Flag的に利用するのなら、FFaasを利用する価値はあります。 おわりに OpenFeatureを中心に、FeatureFlagの現在についてお話ししました。参考になれば幸いです。 BASEでは、今後の事業成長を支えるエンジニアを募集しています。 ご興味があれば、ぜひ採用情報をご覧ください。 採用情報 | BASE, Inc. - BASE, Inc. 明日のBASEアドベントカレンダーは小笠原さんの記事です。お楽しみに!