🫧

【AWS AppConfig】Feature Flagの設計をしてみる【Github Packages】

に公開

概要

Experiment Toggle に特化した Feature Flag の設計についてまとめてみました 🐇
主に「未完成の機能を特定テナントで有効化し、実運用の中でフィードバックや検証を行う」ことを目的として書いています!

前提と用語

要件

前提

https://zenn.dev/rehabforjapan/articles/feature-flag-checklist
上記を元に要件を検討

目的

  • 未完成の機能を本番環境で限定的に公開し、データを基に評価

  • Experiment Toggles を利用し、カナリアリリース・(A/B テスト)を行う

生存期間

2 週間 ~ 数ヶ月

  • 統計的に有意なデータが取れる期間に設定する
  • アプリケーションやユーザー行動の変化による影響を考慮し、1 年以上フラグが残り続けるといった長期実施は避ける

変更粒度

  • 機能単位

    • 新機能のオン / オフ

    • API レスポンスの変更

  • UI 単位

    • フォームの自動入力

    • ボタンのテキスト変更

制御単位

  • テナント単位

変更頻度

  • デプロイごとに設定する(静的)

    • テナントを設定ファイルに追加し、静的な管理を行えるため導入が容易

対象範囲

  • FrontEnd, Backend, リポジトリ(*それぞれ言語が異なる)

計測対象

ユーザーインタビュー

  • 実施したテナントに対して Google Form でアンケートを作成し入力してもらう。

ユーザー行動

  • コンバージョン率

    • 新機能を試したユーザーのうち実際に主要なアクションを完了した割合
  • クリック率

    • 新機能を認知したユーザのうち新機能を試した割合

      • 離脱がどれだけあるか
  • 滞在時間

  • エラーレート

  • 継続率

    • 新機能を使い続ける割合

      • 「使う」の定義は機能によって異なる

削除

  • フラグ追加時に、期間を指定する

  • 期間を過ぎた時に削除を促す

    • Github Actions などで追加時に削除リマインダーを設定する
  • 削除は FrontEnd, Backend ともに実施する

Flag 種類 用途 責任
Experiment Toggles 未完成の機能を本番環境で限定的に公開して評価 各プロダクト・開発チーム
Permission Toggles 永続的なロール・パーミッション管理(※今回は対象外) 🙅‍♀️ 今回の要件では利用しない

システム全体像

  • Feature Flag の設定値はGithub PackagesまたはAWS AppConfigで各リポジトリとは切り離された外部に定義を行う
  • Backend API で AppConfig を取得し制御を行う
  • Backend API で Feature Flag 専用のエンドポイントを作成し AppConfig の設定値を Frontend へ返却する

Github Packages

AWS AppConfig

JSON vs AppConfig

JSON vs AWS App Config

項目 JSON ファイル管理 AWS AppConfig
管理場所 Git リポジトリ内の feature_flags.json など AWS AppConfig のアプリケーション設定
反映タイミング デプロイが必要(ファイル変更 → 再ビルド/再デプロイ) 即時反映可能(デプロイ不要、設定更新 → 自動反映)
環境ごとの切替 手動で環境をファイルに定義 AppConfig の環境機能により切替可能(dev/stg/prod など)
可視性(UI 管理) GitHub などのコードレビューで可視化 AWS マネジメントコンソール上で UI 管理可能
監査ログ Git 履歴で確認可能 CloudTrail や AppConfig 履歴で確認可能
ロールバック Git でバージョン戻し
切り戻し時に、機能の公開/ひこうかいの状態も切り替わる可能性がある
AppConfig のバージョンを切り替えて即時ロールバック可能
動的変更 不可 動的変更が可能(アプリは定期取得 or ポーリングなどで対応)
導入コスト 低い(シンプルで手軽) 中程度(IAM ロールや AppConfig 設定が必要)
セキュリティ アプリの権限で自由に参照可能 IAM ポリシーで参照制御可能
リアルタイム性 低い 高い(即時切替・通知設定も可能)

Github Packages

GitHub Packages 経由で Flag を配布する
また、各 product で共通参照可能な構成とするため json を出力する

命名規則

product 毎に json ファイルを分割し、key = {newFeature} とする

環境制御

各フラグには env フィールドを定義し、dev, prod などの環境別制御を可能にする

const flags = {
  newApiFeature: {
    targets: [1000, 1001],
    env: ["dev", "prod"],
  },
};

パッケージ構成

  • 内部では型安全とするため typescript で定義し main マージや build 時に json に変換する
feature-flags/
├── package.json
├── products/
│   ├── productA.ts
│   ├── productB.ts
│   └── productC.ts
├── tenants.ts     # 共通テナント定義
├── index.ts       # 全フラグをまとめて export

フラグ定義構造例

productA.ts
const flags = {
  newApiFeature: {
    targets: [1000, 1001],
    expiresAt: "2025-04-30",
    env: ["dev", "prod"],
    meta: {
      description: "APIの試験公開",
      createdAt: "2025-04-01",
      createdBy: "userA"
    }
  },
  newUiFeature: {
    targets: [1001],
    expiresAt: "2025-06-30",
    env: ["prod"],
    meta: {
      description: "UIの試験公開",
      createdAt: "2025-04-01",
      createdBy: "userA"
    }
  }
}
tenants.ts
const tenants = {
  "betaGroup": [1000, 1001, 1002],
  "internal": [9999] # 開発環境
}

型定義

  • Union 型や zod / io-ts を使って型安全に定義
types.ts
export type FlagKey = 'newApiFeature' | 'newUiFeature';

export const featureSchema = z.object({
  targets: z.array(z.number()),
  expiresAt: z.string(),
  env: z.array(z.string()),
  meta: z.object({
    description: z.string(),
    createdAt: z.string().optional(),
    createdBy: z.string().optional()
  }).optional()
});

AWS AppConfig

リソースタイプ

AppConfig 次の要素が存在する

  • application
    • プロダクト単位
  • environment
    • dev, staging, main など
  • configuration profile
    • feature flag json (flag の設定値)

https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_awsappconfig.html#awsappconfig-resources-for-iam-policies

構成

各プロダクト毎に独立管理を行う

AppConfig
└── Application: product-a
    ├── Environment: production
    └── ConfigurationProfile: feature-flags
└── Application: product-b
    ├── Environment: production
    └── ConfigurationProfile: feature-flags

フラグ定義構造例

{
  "version": "2025-05-01",
  "flags": {
    "newApiFeature": {
      "description": "新しい検索機能を有効化",
      "targets": ["tenant-001", "tenant-005"],
      "expiresAt": "2025-07-01"
    },
    "newUiFeature": {
      "description": "フォーム自動入力",
      "targets": ["tenant-003"],
      "expiresAt": "2025-06-15"
    }
  }
}

バリデーション

AppConfig のValidator機能を活用に安全にフラグを定義する

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "featureFlags": {
      "type": "object",
      "patternProperties": {
        "^[a-z0-9-]+$": {
          "type": "object",
          "properties": {
            "enabled": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^tenant-[0-9]{3}$"
              }
            },
            "expiresAt": {
              "type": "string",
              "format": "date"
            }
          },
          "required": ["enabled", "expiresAt"]
        }
      }
    }
  },
  "required": ["featureFlags"]
}

管理方法

フラグの変更は AWS CDK, Cloudformation で管理を行う

Flag のデフォルト値

以下のようにフラグが無効になる場合は Flag の値はデフォルトでfalseとする
→ 機能がオフとなる

  • フラグが設定されていない
  • 意図しないエラーによりフラグが取得できない
  • など

Feature Flag の切り替え制御方法

今後の拡張を考えた時、現状は静的で良いが動的管理への移行が必要になった際に変更容易としたいため各プロダクトで以下のような FeatureFlagProvider インターフェースを定義し、DI によって抽象化しておく

以下は Java(Spring Boot)の例

public class FeatureFlag {
    private List<String> enabledTenants;
    private LocalDate startAt;
    private LocalDate endAt;

    public boolean isEnabledFor(String tenantId) {
        LocalDate now = LocalDate.now();
        return enabledTenants.contains(tenantId) &&
               (startAt == null || !now.isBefore(startAt)) &&
               (endAt == null || !now.isAfter(endAt));
    }
}
public class FeatureFlagConfig {
    private Map<String, FeatureFlag> features;

    public boolean isEnabled(String featureName, String tenantId) {
        FeatureFlag flag = features.get(featureName);
        return flag != null && flag.isEnabledFor(tenantId);
    }
}
@Component
public class FeatureFlagProvider {

    private final ObjectMapper objectMapper;
    private final AWSSimpleSystemsManagement ssmClient; // AWS SDK
    private FeatureFlagConfig config;

    public FeatureFlagProvider(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @PostConstruct
    public void init() {
        this.config = fetchFromAppConfig();
    }

    public boolean isEnabled(String featureName, String tenantId) {
        return config.isEnabled(featureName, tenantId);
    }

    private FeatureFlagConfig fetchFromAppConfig() {
        // 例: AppConfig の HTTP エンドポイントから JSON を取得
        String json = fetchJsonFromAppConfig(); // REST Template / WebClient 等を使う
        try {
            return objectMapper.readValue(json, FeatureFlagConfig.class);
        } catch (IOException e) {
            throw new RuntimeException("Failed to parse AppConfig feature flags", e);
        }
    }
}
@Service
public class SomeBusinessService {

    private final FeatureFlagProvider flagProvider;

    public SomeBusinessService(FeatureFlagProvider flagProvider) {
        this.flagProvider = flagProvider;
    }

    public void doSomething(String tenantId) {
        if (flagProvider.isEnabled("new_feature_x", tenantId)) {
            // 新機能の処理
        } else {
            // 従来の処理
        }
    }
}

削除フロー

  • 各フラグ定義に expiresAt(適用終了日)を設定
  • production 適用時に GitHub Actions を発火
  • 適用終了日に Slack へ削除リマインド通知を送信

Discussion