Next.jsでAWS App Configを使ってFeature Flags実装してみた
Feature Flags とは
Feature Flags (Feature Toggle)は、特定の機能や機能の一部を動的に有効化、または無効化することができる仕組みを指します。
これにより、ソフトウェアの振る舞いを変更することができますが、その変更はコードの変更や再デプロイを伴うことなく、リアルタイムに行うことができます。
デプロイ頻度と Feature Flags
Feature Flags を使用すると、新しい機能はデフォルトで無効化したまま本番環境にデプロイすることが可能となります。
これを行うことで公開したくない機能のためにブランチをマージ待ちにする必要がなくなるため、デプロイ頻度やリードタイムの向上が見込めます。
デプロイ頻度やリードタイムの向上は Four Keys というソフトウェア開発チームのパフォーマンスを計測する 4 つの指標のうちの 2 つであり、チームの生産性向上に役立ちます。
Terraform で実装
AWS App Config は Application
という枠組みの中に Configuration Profile(Feature Flags)
を作成して、Environment
ごとにフラグのデプロイを行えます。
実際の flag の値は手動でいじりたかったので、Application
、Configuration Profile
とEnvironment
を 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 という型定義のファイルを用意し、手動で追加するフラグを管理します。
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
をラップする形で取得する関数を定義します。
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()
という関数でフラグを取得します。
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 に値を取りに行っているのは以下のコードです。
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;
};
ApplicationIdentifier
、EnvironmentIdentifier
、ConfigurationProfileIdentifier
には先程 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
}
environment
とconfiguration_profile
は単に id を渡してしまうとapplication
の id を含んだ形で渡ってしまうので注意してください。
切り替えてみる
作成されたApplication
からプロファイルを選択し+ Add new flag
からフラグを追加して Save します。その後右上のStart deployment
をクリックします。
Environment
とバージョンを指定し、Deployment strategy
に先程 terraform で作成したストラテジーを選択してデプロイします。
Deployments
の欄に新しいバージョンが追加され、State が Complete になっていれば新しいフラグが公開されています。
まとめ
とりあえず AWS でサービスをまとめつつ、Feature Flags を導入したいと思ったので、かなり手軽に導入できそうな App Config の Feature Flags を試してみましたが、期待通り簡単に導入することが出来ました。
もう少し細かな制御をするには Amazon CloudWatch Evidently を使ったほうが良いのかな?と思いつつ現状はやりたいことが実現できているので、しばらくこれで運用してみようと思います。
Discussion