🚩

Next.jsでAWS App Configを使ってFeature Flags実装してみた

2023/10/31に公開

Feature Flags とは

Feature Flags (Feature Toggle)は、特定の機能や機能の一部を動的に有効化、または無効化することができる仕組みを指します。
これにより、ソフトウェアの振る舞いを変更することができますが、その変更はコードの変更や再デプロイを伴うことなく、リアルタイムに行うことができます。

デプロイ頻度と Feature Flags

Feature Flags を使用すると、新しい機能はデフォルトで無効化したまま本番環境にデプロイすることが可能となります。
これを行うことで公開したくない機能のためにブランチをマージ待ちにする必要がなくなるため、デプロイ頻度やリードタイムの向上が見込めます。

デプロイ頻度やリードタイムの向上は Four Keys というソフトウェア開発チームのパフォーマンスを計測する 4 つの指標のうちの 2 つであり、チームの生産性向上に役立ちます。

https://dora.dev/

https://site.developerproductivity.dev/feature-flags-abtest-products-202206/

Terraform で実装

AWS App Config は Applicationという枠組みの中に Configuration Profile(Feature Flags)を作成して、Environmentごとにフラグのデプロイを行えます。
実際の flag の値は手動でいじりたかったので、ApplicationConfiguration ProfileEnvironmentを prd/stg と用意し、Deployment Strategyとして即時に値を反映するもののみを Terraform で追加しました。

// https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appconfig_application
resource "aws_appconfig_application" "flags" {
  name        = "${var.project}-config"
  description = "feature flags"
}

resource "aws_appconfig_environment" "stg" {
  name           = "stg"
  application_id = aws_appconfig_application.flags.id
  description    = "feature flags for stg"
}

resource "aws_appconfig_environment" "prd" {
  name           = "prd"
  application_id = aws_appconfig_application.flags.id
  description    = "feature flags for prd"
}

resource "aws_appconfig_configuration_profile" "feature_flags" {
  application_id = aws_appconfig_application.flags.id
  name           = "feature-flags"
  location_uri   = "hosted"
  type           = "AWS.AppConfig.FeatureFlags"
}

// 即時実行するストラテジーのみ用意する
resource "aws_appconfig_deployment_strategy" "strategy" {
  name                           = "${var.project}-quick-deploy"
  description                    = "deployment strategy"
  deployment_duration_in_minutes = 0
  final_bake_time_in_minutes     = 0
  growth_factor                  = 100
  replicate_to                   = "NONE"
}

Next.js 側の実装

今回既存の PJ に追加実装したため、pages ルーター前提で記載しています。

Next.js ではまず flags.ts という型定義のファイルを用意し、手動で追加するフラグを管理します。

src/types/flags.ts
import { z } from 'zod';

// 手動で設定する App ConfigのFeature Flagsと型を合わせる
export const featureFlags = z.object({
  someFlags: z.object({ enabled: z.boolean(), someAttribute: z.string() }),
});

export type FeatureFlags = z.infer<typeof featureFlags>;

実際の App Config の Feature Flags からはこれ以外にもプロパティが渡ってくるので、必要に応じて受け取るようにしてください。

そしてカスタム hooks としてswrをラップする形で取得する関数を定義します。

useFeatureFlags.ts
import { clientConfig } from '@/constants/config';
import { apiClient } from '@/hooks/apiClient';
import { FeatureFlags } from '@/types/flags';
import useSWR from 'swr';

export const useFeatureFlags = () => {
  const fetcher = (url: string) => apiClient.get(url).then((res) => res.data);
  const { data, error, isLoading } = useSWR<FeatureFlags>(
    `${clientConfig.baseUrl}/api/flags`,
    fetcher
  );

  return {
    flags: data,
    isLoading,
    isError: error,
  };
};

api ルート経由で取得しているので、api ルート側では実際にgetFeatures()という関数でフラグを取得します。

src/pages/api/flags/index.ts
import { getFeatures } from '@/clients/AppConfigClient';
import { featureFlags } from '@/types/flags';
import { NextApiRequest, NextApiResponse } from 'next';

// FeatureFlagsを返すエンドポイント
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
  const response = await getFeatures();
  const flags = featureFlags.parse(response);

  res.status(200).json({ ...flags });
}

App Config に値を取りに行っているのは以下のコードです。

AppConfigClient.ts
import { localFeatureFlags } from '@/constants/config';
import {
  AppConfigDataClient,
  GetLatestConfigurationCommand,
  StartConfigurationSessionCommand,
  StartConfigurationSessionCommandInput,
} from '@aws-sdk/client-appconfigdata';

const client = new AppConfigDataClient({ region: 'ap-northeast-1' });

export const getFeatures = async () => {
  if (process.env.NEXT_PUBLIC_APP_ENV === 'local') return localFeatureFlags;
  const input: StartConfigurationSessionCommandInput = {
    ApplicationIdentifier: process.env.APP_CONFIG_APPLICATION as string,
    EnvironmentIdentifier: process.env.APP_CONFIG_ENVIRONMENT as string,
    ConfigurationProfileIdentifier: process.env
      .APP_CONFIG_CONFIGURATION as string,
  };

  const session = await client.send(
    new StartConfigurationSessionCommand(input)
  );
  const data = await client.send(
    new GetLatestConfigurationCommand({
      ConfigurationToken: session.InitialConfigurationToken,
    })
  );
  if (!data.Configuration) {
    throw new Error('Failed App Config params');
  }
  const config = JSON.parse(data.Configuration.transformToString());

  return config;
};

ApplicationIdentifierEnvironmentIdentifierConfigurationProfileIdentifierには先程 terraform で作成したものを環境変数で渡しています。
terraform の output は以下です。

output "appconfig_application" {
  value = aws_appconfig_application.flags.id
}

output "appconfig_environment_stg" {
  value = aws_appconfig_environment.stg.environment_id
}

output "appconfig_environment_prd" {
  value = aws_appconfig_environment.prd.environment_id
}

output "appconfig_configuration_profile" {
  value = aws_appconfig_configuration_profile.feature_flags.configuration_profile_id
}

environmentconfiguration_profileは単に id を渡してしまうとapplicationの id を含んだ形で渡ってしまうので注意してください。

切り替えてみる

作成されたApplicationからプロファイルを選択し+ Add new flagからフラグを追加して Save します。その後右上のStart deploymentをクリックします。
flag value

Environmentとバージョンを指定し、Deployment strategyに先程 terraform で作成したストラテジーを選択してデプロイします。
start deployment

Deploymentsの欄に新しいバージョンが追加され、State が Complete になっていれば新しいフラグが公開されています。
environment details

まとめ

とりあえず AWS でサービスをまとめつつ、Feature Flags を導入したいと思ったので、かなり手軽に導入できそうな App Config の Feature Flags を試してみましたが、期待通り簡単に導入することが出来ました。
もう少し細かな制御をするには Amazon CloudWatch Evidently を使ったほうが良いのかな?と思いつつ現状はやりたいことが実現できているので、しばらくこれで運用してみようと思います。

https://aws.amazon.com/jp/blogs/aws/cloudwatch-evidently/

Discussion