OpenFeature x AWS AppConfigでサーキットブレーカーを作ろう

これは、[Wano Group Advent Calendar](https://qiita.com/advent-calendar/2024/wano-group) 2024の6日目の記事です。
5日目は、[@NaotoFushimi](https://qiita.com/NaotoFushimi)さんの、[RemixアプリをALB Lambdaとしてデプロイする](https://qiita.com/NaotoFushimi/items/9631bc95a5e2708b1050)です。


# 概要

弊社では、カラオケやミュージックビデオの配信ができるサービス[Video Kicks](https://www.tunecore.co.jp/video-kicks-karaoke)を開発・運営しています。

今回はOpenFeature x AWS AppConfigでサーキットブレーカーを実装するうえで、調査検討・実践したことをまとめていきます。
新機能リリースの際に、運用上その機能を一時的に利用停止状態にしたい場合があります。
それを可能にするのが、サーキットブレーカーです。

# サーキットブレーカーとは

あるアプリケーションが異常状態である場合や、リクエストを送信してもエラーが発生することが分かっている時、そもそもリクエスト遮断して送信しないようにするものです。

### 今回でいう異常状態とは

リリースした新機能の利用者が多すぎて、社内で対応できる人的リソースが不足した時とします。

例・・・ひと月あたりの利用者数がxxx件を超えたら、受付を一時的に停止する。

サーキットブレーカーは3つの状態を持ちますが、導入した場合の流れは以下のようになります。

- **CLOSED（正常）**
新機能の利用受付可能な状態。

- **OPEN（異常）**
    新機能利用者数が閾値を超えたので、**受付をストップしている状態**。特定の条件を満たすことで移行する。
    運用者用の管理画面のUIからの操作や、利用者数を監視して自動的に移行させるなどの方法がある。

- **HALF-OPEN（今回は利用しない）**
代行依頼受付を再開してもよい状態に復帰したかを確認し、OKならCLOSED（正常）に移行する。
    

以下記事の説明を読み替えています。

- 参考：[Apache Camelでサーキットブレーカーパターン（Circuit Breaker Pattern）](https://qiita.com/mkyz08/items/4412be9557517b2427f6)
    
    > サーキットブレーカーは以下の3つの状態を持ちます。
    > 
    > - CLOSED
    >     
    >     正常な状態。直近のエラー数をカウントし、その回数が閾値を超えるとOPENに移行します。
    >     
    > - OPEN
    >     
    >     エラーによりリクエストを制限している状態。一定時間経過することでHALF-OPENに移行します。
    >     
    > - HALF-OPEN
    >     
    >     リモートシステムが障害から回復したかを確認する。少数のリクエストを実行し処理が成功すればCLOSED（正常）に移行します。
    > 

# FeatureFlagとは

コードを書き換えることなく、特定の機能を動的に有効化したり無効化することができる開発手法です。　

今回は、新機能利用の受付を停止・再開するのに利用します。

## FeatureFlagの種類

### 1.　Release Toggles
    
新機能のコードをデプロイしておいて、ユーザーに提供するタイミングにトグルを切り替えてリリースします。

featureブランチをmaster(main)ブランチに統合しやすく、ブランチが腐ることを防げるなどメリットがあります。
    

以下の例のように、**新機能を含むコードをmasterにマージ後デプロイしても、featureFlagを有効化しなければ今までと挙動が変わりません。**
任意のタイミングでfeatureFlagを切り替えることで、コードに変更を加えることなく新機能を有効化できます。

```go
releaseToggle := getToggleValueByAppconfig() // Appconfig等で管理されているフラグの値を取得

if releaseToggle {
	doSomethingNew() // 新機能の処理
} else {
	doSomethingOld() // 旧機能の処理
}
```

### 2.　**Ops Toggles（今回はこれを利用）**
    
運用トグル。サーキットブレーカーとしても利用されます。新機能リリース時に負荷が高まったり、運用面の状況によって担当者がその新機能の無効化・有効化を切り替えることができます。
    
今回は、新機能利用者数が増加しすぎて運用負荷が急増した場合に、一時的に利用受付を停止する用途で用います。
    

```go
opsToggle := getToggleValueByAppconfig() // Appconfig等で管理されているフラグの値を取得

// opsToggleをサーキットブレーカーとして用いている。
// サーキットブレーカーが有効（true）の場合は受付を停止する。
if !opsToggle {
	doSomethingNew() // 新機能の処理
} else {
	return fmt.Errorf("現在、カラオケ代行依頼は受付停止中です。")
}
```

### 3.　Permission Toggles
    
ある機能を無料ユーザーには開放せず、有料ユーザーのみに開放するケースや、ベータリリースで一部ユーザーに新機能を解放する等で用います。
    
フラグの値は、ConfigMapを定義する・DBの特定のカラム値を見る（例・・・User.is_premium）などで分けられます。

- ConfigMapの例
    
```yaml
# 下記ユーザーIDのみON
feature_ids1:
 - 1
 - 5
 - 13
# 全体でON
feature_ids2:
 - ['*']
# 全体でOFF
feature_ids3:
 - []
```
    

```go
user := input.User

configMapIDs := getFetureUserIDsByConfigMapYml()

if slices.Contains(configMapIDs, user.ID) {
	doSomethingNew() // 新機能の処理
} else {
	doSomethingOld() // 旧機能の処理
}
```

### 4.　Experiment Toggles
実験トグル。既存機能やアルゴリズム改善などのためのA/Bテストのように、ユーザー単位・リクエスト単位で機能を切り替えます。
        
フラグの値は、リクエスト単位であればランダム、ユーザー単位であれば奇数ならtrue・偶数ならfalseなどのように分けることができます。
    
    ```go
    experimentToggle := getToggleValue()
    
    if experimentToggle {
    	doSomethingA() // Aパターンの処理
    } else {
    	doSomethingB() // Bパターンの処理
    }
    ```
    

# AWS AppConfigとは

コードのデプロイなしで本番環境のアプリケーションの挙動を調整できるサービス。

設定により、機能を全ユーザーにデプロイするのではなく、徐々にリリースするようなことも可能となります。

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/what-is-appconfig.html

> **機能フラグとトグル** — 管理された環境で、新しい機能を安全に顧客にリリースできます。問題が発生した場合は、変更を即座にロールバックできます。
> 

今回は以下のようなことの実現を目指します。※OpenFeatureについては後述

```mermaid
sequenceDiagram
    participant App as アプリケーション
    participant OpenFeature as OpenFeature Client（SDK + Provider）
    participant AppConfig as AWS AppConfig

    App ->> OpenFeature: フィーチャーフラグの値を取得リクエスト
    OpenFeature ->> AppConfig: フィーチャーフラグの値を参照
    AppConfig -->> OpenFeature: フィーチャーフラグの値を返す
    OpenFeature -->> App: フィーチャーフラグの値を返す

    App ->> OpenFeature: フィーチャーフラグの値を更新リクエスト
    OpenFeature ->> AppConfig: フィーチャーフラグの値を更新
    AppConfig -->> OpenFeature: 更新結果を返す
    OpenFeature -->> App: 更新結果を返す

```

# OpenFeatureとは

https://openfeature.dev/docs/reference/intro/

OpenFeatureは、機能フラグ管理のオープンな標準であり、特定のベンダー依存なしにAPIを定義したりSDKを提供します。

## OpenFeature Provider

アプリケーションはOpenFeatureのSDK（汎化されたAPI）を呼び出すことで、その裏側のProviderが固有のバックエンドからフィーチャーフラグ値を取得する仕組みとなります。つまり、Providerがフィーチャーフラグのために利用するサービスの差異を吸収してくれます。

したがってこれを利用することで、フィーチャーフラグを管理するサービスを移行したい時などに変更箇所が少なくて済むという利点があります。


![スクリーンショット 2024-11-01 13.25.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/040c3e4d-8222-d5c5-9f2d-fc22d8534adc.png)


## OpenFeature ProviderにはAppConfig対応のProviderがない…

しかし、2024/12/04時点でAppConfig対応のProviderは用意されていません。

[OpenFeature Providerが対応しているFeatureFlag系サービス](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Go)

openfeature.FeatureProviderを実装した**AppConfig用のProviderを自分で書く必要があります。**

実装例が以下の通り記載されているため、これを参考に実装してみます。

https://openfeature.dev/docs/reference/concepts/provider#dynamic-context-implementations-server-side-sdks

```go
package provider

// MyFeatureProvider implements the FeatureProvider interface and provides functions for evaluating flags
type MyFeatureProvider struct{}

// Metadata returns the metadata of the provider
func (e MyFeatureProvider) Metadata() Metadata {
    return Metadata{Name: "MyFeatureProvider"}
}

// BooleanEvaluation returns a boolean flag
func (e MyFeatureProvider) BooleanEvaluation(flag string, defaultValue bool, evalCtx EvaluationContext) BoolResolutionDetail {
    // code to evaluate boolean
}

// StringEvaluation returns a string flag
func (e MyFeatureProvider) StringEvaluation(flag string, defaultValue string, evalCtx EvaluationContext) StringResolutionDetail {
    // code to evaluate string
}

// FloatEvaluation returns a float flag
func (e MyFeatureProvider) FloatEvaluation(flag string, defaultValue float64, evalCtx EvaluationContext) FloatResolutionDetail {
    // code to evaluate float
}

// IntEvaluation returns an int flag
func (e MyFeatureProvider) IntEvaluation(flag string, defaultValue int64, evalCtx EvaluationContext) IntResolutionDetail {
    // code to evaluate int
}

// ObjectEvaluation returns an object flag
func (e MyFeatureProvider) ObjectEvaluation(flag string, defaultValue interface{}, evalCtx EvaluationContext) ResolutionDetail {
    // code to evaluate object
}

// Hooks returns hooks
func (e MyFeatureProvider) Hooks() []Hook {
    // code to retrieve hooks
}
```

# goで実装してみた

## AppConfigによるFeatureFlag作成のための実装に必要な内容

### 1.　アプリケーションの作成

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-namespace.html

使用中のアプリケーションの単なる名前空間です。例えば、FeatureFlagを利用するアプリ名などでよいでしょう。

### 2.　設定プロファイルの作成

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-configuration-profile.html

アプリケーションの動作に影響を与える設定の一覧です。例えば、どの機能に影響があるかわかる名前でいいでしょう。

種別としては

- 機能フラグ（今回はこちらを使用）
- フリーフォーム設定

が選択できます。


### 3.　環境の作成

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-environment.html

> AWS AppConfig アプリケーションごとに、1 つ以上の環境を定義します。
環境は、 `Beta`または `Production`環境のアプリケーション、 AWS Lambda 関数、コンテナなど、 
AppConfig ターゲットの論理的なデプロイグループです。
`Web`‬、`Mobile`‬、および `Back-end`‬ など、アプリケーションのサブコンポーネントの環境を定義することもできます。
> 

例えば、「このフラグのこのバージョンを、この環境にデプロイする」のように、フラグのデプロイ先の指定に使用します。

また、WebとMobileそれぞれの環境に別の設定をデプロイしておくことで、
ユーザーの利用環境によって参照するAppConfigのフラグを切り替えるような使い方も可能です。

※デプロイとは、アプリからAppConfigのフラグ設定を参照できるようにすることです。

### 4.　フラグ有効・無効 各バージョンの作成（フィーチャーフラグ切り替えのために必要）

機能フラグ設定プロファイルを保存するたびに、新しいバージョン番号が作成されます。つまり、あるフラグの設定値を変更し保存する時、新しいバージョンが作成されることになります。

今回は、フィーチャーフラグが有効・無効の2パターンが必要であるため2つのバージョンを保存し、デプロイ時に指定することで切り替えます。

### 5.　デプロイ戦略の作成

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-creating-deployment-strategy-create.html

事前定義されたデプロイ戦略を利用することもできるが、自身で作成することもできます。

設定できる項目の例は以下のようなものがあります。

- **Name**・・・・・デプロイ戦略名
- **Description**・・・説明
- **DeploymentDurationInMinutes**
    デプロイが完了するまでにかかる時間（分）。今回は即時デプロイ反映したいため0分とします
    
- **GrowthFactor**
    各デプロイのステップ毎の完了増加率。今回は即時デプロイし反映たいため100%とします
- **GrowthType**
    GrowthFactorの増加タイプ。linearまたはexponentialが選択可能です
    - linear・・・・・線形的な増加。GrowthFactorが20であれば、ステップごとに20%ずつ増加します
    - exponential・・・指数的な増加`G*(2^N)`の式のように増加する。GrowthFactorが2であれば、2、4、8、16…のように増加します
- **FinalBakeTimeInMinutes**
    デプロイ完了後（100%になった後）にCloudWatchアラームが発生したときに自動ロールバックをするために、CloudWatchアラームを監視する時間
    

```go
Name:                        aws.String(string(FeatureFlagDeployStrategyNameApplyImmediately)),
DeploymentDurationInMinutes: aws.Int32(0),     // デプロイが完了するまでにかかる時間（分）。今回は即時デプロイ反映したいため0分とする
GrowthFactor:                aws.Float32(100), // 各デプロイのステップ毎の完了増加率。今回は即時デプロイし反映たいため100%とする。
Description:                 aws.String(""),
GrowthType:                  "linear", // GrowthFactorの増加タイプ。linearまたはexponentialが選択可能。
FinalBakeTimeInMinutes:      0, // デプロイ完了後にCloudWatchアラームが発生したときに自動ロールバックをするために、CloudWatchアラームを監視する時間
```

### 6-1.　デプロイ（4で作成した設定を、アプリから参照可能な状態にすること）

https://docs.aws.amazon.com/ja_jp/appconfig/latest/userguide/appconfig-deploying.html

ここでは、4で作成したフラグの設定をデプロイし、アプリから参照可能な状態にします。

デプロイには、1-3および5で作成した項目のIDを用います。

設定項目は主に以下の通りです。

```go
		ApplicationId:          app.Id, // 1で作成したアプリケーションのID
		ConfigurationProfileId: profile.Id,　// 2で作成した設定プロファイルのID
		EnvironmentId:          env.Id, // 3で作成した環境のID
		ConfigurationVersion:   aws.String(string(configVersion)), // 4で作成したフラグのうち、今回デプロイしたいバージョン
		DeploymentStrategyId:   strategy.Id, // 5で作成したデプロイ戦略のID
```

追加で、メタデータや説明を含めることもできます。

## 6-2.　FeatureFlagの切り替え（再デプロイ）

デプロイ時に選択する選択を変更して、6を再度実行します。

今回は、現在デプロイされているものとは異なるConfigurationVersion（ex. 有効=1、無効=2）を選択してデプロイします。

デプロイ完了後、アプリから参照されるFeatureFlagの設定が変更されていることが確認できます。



# 実装例

```go
package open_feature_provider

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/appconfig"
	"github.com/aws/aws-sdk-go-v2/service/appconfigdata"
	"github.com/labstack/gommon/log"
	"github.com/open-feature/go-sdk/openfeature"
	"github.com/xxxx/v2/config"
	"github.com/xxxx/aws_sdk_v2"
)

type AppConfigProvider struct {
	Dependency
	client  aws_sdk_v2.AppConfig
	envName AppConfigEnv
	Conf  config.Config
}

type options struct {
	client aws_sdk_v2.AppConfig
}

// 色々なフラグが乱立するのを防ぐために、フラグ名を定数で管理する
type FeatureFlagKeyName string
type FeatureFlagAppName string

// スネークケースで定義してください。"FeatureFlag"のような大文字で始まる名前を使用するとJSON Schema Validationでエラーになります。
const (
	FeatureFlag FeatureFlagKeyName = "feature_flag"
)
const (
	SampleFlagAppName FeatureFlagAppName = "sample_flag_app_name"
)

type FeatureFlagDeployStrategyName string

const (
	FeatureFlagDeployStrategyNameApplyImmediately FeatureFlagDeployStrategyName = "applyImmediately"
)

type AppConfigType string

const (
	AppConfigTypeFeatureFlags AppConfigType = "AWS.AppConfig.FeatureFlags"
	AppConfigTypeFreeform     AppConfigType = "AWS.Freeform"
)

type AppConfigEnv string

const (
	AppConfigEnvStaging    AppConfigEnv = "staging"
	AppConfigEnvProduction AppConfigEnv = "production"
)

// フィーチャーフラグの有効化状態
// AppConfigではフラグの状態を変更するたびにバージョン作成が必要となる。
// このproviderで定義されるフィーチャーフラグではenabled/disabledの2つの状態のみをサポートし、
// それらのバージョンを切り替えることでバージョンが無限に増加するのを防ぐ。
type FeatureFlagVersion string

const (
	FeatureFlagVersionEnabled  FeatureFlagVersion = "1"
	FeatureFlagVersionDisabled FeatureFlagVersion = "2"
)

func getFlagVersionByFlagValue(val bool) FeatureFlagVersion {
	if val {
		return FeatureFlagVersionEnabled
	}
	return FeatureFlagVersionDisabled
}

type FeatureFlagConfig struct {
	FeatureFlag struct {
		Enabled bool `json:"enabled"`
	} `json:"feature_flag"`
}

type Option interface {
	apply(opts *options)
}

type clientOption struct {
	client aws_sdk_v2.AppConfig
}

func (o clientOption) apply(opts *options) {
	opts.client = o.client
}

func WithClientOption(client aws_sdk_v2.AppConfig) clientOption {
	return clientOption{client: client}
}

type Dependency struct {
	Conf config.Config
}

func New(deps Dependency, opts ...Option) OpenFeatureProvider {
	ctx := context.TODO()
	client, err := aws_sdk_v2.CreateGetAppConfigInstanceDefault(ctx)
	if err != nil {
		log.Panic(err)
	}

	options := &options{
		client: client,
	}

	for _, o := range opts {
		o.apply(options)
	}

	getEnvName := func() AppConfigEnv {
		if deps.Conf.VkEnv == "production" {
			return AppConfigEnvProduction
		}
		return AppConfigEnvStaging
	}

	return &AppConfigProvider{
		Dependency: deps,
		client:     options.client,
		envName:    getEnvName(),
	}
}

func (p *AppConfigProvider) Metadata() openfeature.Metadata {
	return openfeature.Metadata{
		Name: "AWSAppConfig",
	}
}

// AppConfigにフィーチャーフラグを新規作成する
// 1. アプリケーションの作成
// 2. 設定プロファイルの作成
// 3. 環境の作成
// 4. フィーチャーフラグの作成
// 5. デプロイ戦略の作成
// 6. デプロイ（1-4で作成した設定を、アプリから参照可能な状態にすること）
func (p *AppConfigProvider) InitFeatureFlag(ctx context.Context, appName FeatureFlagAppName, description *string, flagName FeatureFlagKeyName, flagValue bool) error {

	// 1. アプリケーションの作成
	appResp, err := p.client.CreateApp(ctx, &appconfig.CreateApplicationInput{
		Name:        aws.String(string(appName)),
		Description: description,
	})
	if err != nil {
		return err
	}
	if appResp == nil {
		return fmt.Errorf("appResp is nil")
	}
	// 2. 設定プロファイルの作成
	profileResp, err := p.client.CreateConfigProfile(ctx, &appconfig.CreateConfigurationProfileInput{
		ApplicationId: appResp.Id,
		Name:          (*string)(&appName),
		Type:          aws.String(string(aws_sdk_v2.AppConfigTypeFeatureFlags)),
		LocationUri:   aws.String("hosted"), // appconfigでホストする
	})
	if err != nil {
		return err
	}
	if profileResp == nil {
		return fmt.Errorf("profileResp is nil")
	}
	// 3. 環境の作成
	envResp, err := p.client.CreateEnvironment(ctx, &appconfig.CreateEnvironmentInput{
		ApplicationId: appResp.Id,
		Name:          aws.String(string(p.envName)),
	})
	if err != nil {
		return err
	}
	if envResp == nil {
		return fmt.Errorf("envResp is nil")
	}

	// 4. フィーチャーフラグの作成
	// フィーチャーフラグデータのJSONスキーマは以下リファレンスを参照
	// https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile-feature-flags.html#appconfig-type-reference-feature-flags
	// 以下JSONは↓を参考に作成
	// https://dev.classmethod.jp/articles/create-feature-flags-in-appconfig-with-aws-cli/
	contentJsonStrEnabled := p.createFeatureFlagJson(FeatureFlagVersionEnabled, flagName, description, true)
	contentJsonStrDisabled := p.createFeatureFlagJson(FeatureFlagVersionDisabled, flagName, description, false)

	// CreateHostedConfigurationVersionに使用するバージョン番号は0から始まる
	var latestVersionNumber int32 = 0
	// 有効・無効状態の2つのバージョンを作成
	p.client.CreateHostedConfigurationVersion(ctx, &appconfig.CreateHostedConfigurationVersionInput{
		ApplicationId:          appResp.Id,
		ConfigurationProfileId: profileResp.Id,
		Content:                []byte(contentJsonStrEnabled),
		ContentType:            aws.String("application/json"),
		LatestVersionNumber:    &latestVersionNumber,
	})

	latestVersionNumber++
	p.client.CreateHostedConfigurationVersion(ctx, &appconfig.CreateHostedConfigurationVersionInput{
		ApplicationId:          appResp.Id,
		ConfigurationProfileId: profileResp.Id,
		Content:                []byte(contentJsonStrDisabled),
		ContentType:            aws.String("application/json"),
		LatestVersionNumber:    &latestVersionNumber,
	})

	// 5. デプロイ戦略の作成
	strategy, err := p.client.CreateDeploymentStrategy(ctx, &appconfig.CreateDeploymentStrategyInput{
		Name:                        aws.String(string(FeatureFlagDeployStrategyNameApplyImmediately)),
		DeploymentDurationInMinutes: aws.Int32(0),     // GrowthFactorで設定した割合のデプロイが完了する時間（分）。即時デプロイ反映したいため0とする
		GrowthFactor:                aws.Float32(100), // DeploymentDurationInMinutes毎のデプロイ完了の増加率。即時デプロイし反映たいため1（100%）とする
		Description:                 aws.String(""),
		FinalBakeTimeInMinutes:      0, // デプロイ完了後にCloudWatchアラームが発生したときに自動ロールバックをするため、CloudWatchアラームを監視する時間
	})
	if err != nil {
		return err
	}

	configVersion := getFlagVersionByFlagValue(flagValue)

	// 6. デプロイ開始
	_, err = p.client.DeployApp(ctx, &appconfig.StartDeploymentInput{
		ApplicationId:          appResp.Id,
		ConfigurationProfileId: profileResp.Id,
		ConfigurationVersion:   aws.String(string(configVersion)),
		DeploymentStrategyId:   strategy.Id,
		EnvironmentId:          envResp.Id,
	})
	if err != nil {
		return err
	}

	return nil
}

// フィーチャーフラグの有効化状態を変更する
func (p *AppConfigProvider) ChangeFeatureFlagStatus(ctx context.Context, appName FeatureFlagAppName, flagName FeatureFlagKeyName, deployStrategyName FeatureFlagDeployStrategyName, flagValue bool) error {

	input, err := p.createStartDeploymentInput(ctx, appName, flagName, deployStrategyName, flagValue)
	if err != nil {
		return err
	}
	if input == nil {
		return fmt.Errorf("create StartDeploymentInput failed")
	}

	_, err = p.client.DeployApp(ctx, input)
	if err != nil {
		return err
	}

	return nil
}

func (p *AppConfigProvider) createStartDeploymentInput(ctx context.Context, appName FeatureFlagAppName, flagName FeatureFlagKeyName, deployStrategyName FeatureFlagDeployStrategyName, flagValue bool) (*appconfig.StartDeploymentInput, error) {

	appId, err := p.getFlagAppIdByName(ctx, appName)
	if appId == nil || err != nil {
		log.Fatalf("アプリケーションIDの取得に失敗しました: %v", err)
		return nil, err
	}

	configProfileId, err := p.getConfigProfileIdByName(ctx, *appId, appName)
	if configProfileId == nil || err != nil {
		log.Fatalf("設定プロファイルIDの取得に失敗しました: %v", err)
		return nil, err
	}

	configVersion := getFlagVersionByFlagValue(flagValue)

	strategyId, err := p.getDeployStrategyIdByName(ctx, deployStrategyName)
	if strategyId == nil || err != nil {
		log.Fatalf("デプロイ戦略IDの取得に失敗しました: %v", err)
		return nil, err
	}

	envId, err := p.getEnvironmentIdByName(ctx, *appId)
	if envId == nil || err != nil {
		log.Fatalf("環境IDの取得に失敗しました: %v", err)
		return nil, err
	}

	return &appconfig.StartDeploymentInput{
		ApplicationId:          appId,
		ConfigurationProfileId: configProfileId,
		ConfigurationVersion:   aws.String(string(configVersion)),
		DeploymentStrategyId:   strategyId,
		EnvironmentId:          envId,
	}, nil

}

func (p *AppConfigProvider) createFeatureFlagJson(ver FeatureFlagVersion, flagName FeatureFlagKeyName, description *string, flagValue bool) string {
	return fmt.Sprintf(`{
		"version": "1",
		"flags": {
			"%v": {
				"name": "%v",
				"description": "%v"
			}
		},
		"values": {
			"%v": {
				"enabled": %v
			}
		}
	}`, flagName, flagName, *description, flagName, flagValue)
}

func (p *AppConfigProvider) IsFeatureFlagEnabledByName(appName FeatureFlagAppName) (*bool, error) {

	ctx := context.TODO()

	flagName := string(FeatureFlag)

	token, err := p.getConfigurationTokenByAppName(ctx, appName)
	if err != nil {
		log.Fatalf("設定トークンの取得に失敗しました: %v", err)
		return nil, err
	}
	if token == nil {
		log.Info("トークンが取得できませんでした。アプリケーション または 設定プロファイルが存在しないため、作成する必要があります")
		return nil, nil
	}

	// 最新の設定を取得
	configInput := &appconfigdata.GetLatestConfigurationInput{
		ConfigurationToken: token,
	}

	configOutput, err := p.client.GetAppConfigDataClient().GetLatestConfiguration(ctx, configInput)
	if err != nil {
		log.Fatalf("設定の取得に失敗しました: %v", err)
		return nil, err
	}
	if configOutput == nil || len(configOutput.Configuration) == 0 {
		log.Info("設定が取得できませんでした")
		return nil, nil
	}

	// 設定データをパースして、フラグの状態を確認
	var featureFlagConfig FeatureFlagConfig
	err = json.Unmarshal(configOutput.Configuration, &featureFlagConfig)
	if err != nil {
		log.Fatalf("JSONの解析に失敗しました: %v", err)
		return nil, err
	}

	isEnabledFlag := false
	// フラグが存在し、有効化されているかをチェック
	if featureFlagConfig.FeatureFlag.Enabled {
		fmt.Printf("フラグ '%s' は有効です\n", flagName)
		isEnabledFlag = true
	} else {
		fmt.Printf("フラグ '%s' は無効です\n", flagName)
	}

	return &isEnabledFlag, nil
}

func (p *AppConfigProvider) getFlagAppIdByName(ctx context.Context, appName FeatureFlagAppName) (*string, error) {
	appId, err := p.client.GetAppIdByName(ctx, string(appName))
	if err != nil {
		return nil, err
	}
	if appId == nil {
		log.Info("アプリケーションが存在しません。新規作成してください。")
		return nil, nil
	}

	return appId, nil
}

func (p *AppConfigProvider) getConfigProfileIdByName(ctx context.Context, appId string, appName FeatureFlagAppName) (*string, error) {

	profileId, err := p.client.GetConfigProfileIdByName(ctx, appId, string(appName))
	if err != nil {
		log.Fatalf("設定プロファイルの一覧取得に失敗しました: %v", err)
		return nil, err
	}
	if profileId == nil {
		log.Info("設定プロファイルが存在しません。新規作成してください。")
		return nil, nil
	}

	return profileId, nil
}

func (p *AppConfigProvider) getEnvironmentIdByName(ctx context.Context, appId string) (*string, error) {

	envId, err := p.client.GetEnvironmentIdByName(ctx, appId, string(p.envName))
	if err != nil {
		log.Fatalf("環境の一覧取得に失敗しました: %v", err)
		return nil, err
	}
	if envId == nil {
		log.Info("環境が存在しません。新規作成してください。")
		return nil, nil
	}

	return envId, nil
}

func (p *AppConfigProvider) getDeployStrategyIdByName(ctx context.Context, strategyName FeatureFlagDeployStrategyName) (*string, error) {

	strategyId, err := p.client.GetDeployStrategyIdByName(ctx, string(strategyName))
	if err != nil {
		log.Fatalf("デプロイ戦略の一覧取得に失敗しました: %v", err)
		return nil, err
	}
	if strategyId == nil {
		log.Info("デプロイ戦略が存在しません。新規作成してください。")
		return nil, nil
	}

	return strategyId, nil
}

// AppConfigの設定を参照するためのトークンをフラグ名から取得する
func (p *AppConfigProvider) getConfigurationTokenByAppName(ctx context.Context, appName FeatureFlagAppName) (*string, error) {

	token, err := p.client.GetConfigurationTokenByAppName(ctx, string(appName), string(appName), string(p.envName))
	if err != nil {
		log.Fatalf("設定トークンの取得に失敗しました: %v", err)
		return nil, err
	}

	return token, nil
}

// bool値のみをサポートし、ほかの型の評価はエラーを返します
func (p *AppConfigProvider) BooleanEvaluation(ctx context.Context, appName string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
	isEnabledFlag, err := p.CheckIsFeatureFlagEnabledByName(FeatureFlagAppName(appName))

	if err != nil {
		return openfeature.BoolResolutionDetail{
			Value: defaultValue,
			ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
				Reason:          openfeature.ErrorReason,
				ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
			},
		}
	}

	if isEnabledFlag == nil {
		return openfeature.BoolResolutionDetail{
			Value: defaultValue,
			ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
				Reason:          openfeature.UnknownReason,
				ResolutionError: openfeature.NewGeneralResolutionError("フラグが存在しません。"),
			},
		}
	}

	return openfeature.BoolResolutionDetail{
		Value: *isEnabledFlag,
		ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
			Reason: openfeature.UnknownReason,
		},
	}
}

func (p *AppConfigProvider) StringEvaluation(ctx context.Context, flagName string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
	return openfeature.StringResolutionDetail{
		Value: defaultValue,
		ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
			Reason:          openfeature.ErrorReason,
			ResolutionError: openfeature.NewTypeMismatchResolutionError("StringEvaluation はサポートされていません"),
		},
	}
}

func (p *AppConfigProvider) FloatEvaluation(ctx context.Context, flagName string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
	return openfeature.FloatResolutionDetail{
		Value: defaultValue,
		ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
			Reason:          openfeature.ErrorReason,
			ResolutionError: openfeature.NewTypeMismatchResolutionError("FloatEvaluation はサポートされていません"),
		},
	}
}

func (p *AppConfigProvider) IntEvaluation(ctx context.Context, flagName string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
	return openfeature.IntResolutionDetail{
		Value: defaultValue,
		ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
			Reason:          openfeature.ErrorReason,
			ResolutionError: openfeature.NewTypeMismatchResolutionError("IntEvaluation はサポートされていません"),
		},
	}
}

func (p *AppConfigProvider) ObjectEvaluation(ctx context.Context, flagName string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
	return openfeature.InterfaceResolutionDetail{
		Value: defaultValue,
		ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
			Reason:          openfeature.ErrorReason,
			ResolutionError: openfeature.NewTypeMismatchResolutionError("ObjectEvaluation はサポートされていません"),
		},
	}
}

func (p *AppConfigProvider) Hooks() []openfeature.Hook {
	return nil
}

```

# まとめ

今回はOpenFeature x AWS AppConfigでサーキットブレーカーを作ってみました。
OpenFeatureを利用することで入力・出力を固定化できるため、FeatureFlag関連サービスを切り替える際に、利用するアプリケーション側の変更が不要となり変更が楽になります。

皆さんもよいFeatureFlagライフを！

# 人材募集

弊社グループでは一緒に働くメンバーを募集中です、ご応募お待ちしています！

https://herp.careers/v1/wano