AWS LambdaでAWS AppConfigのフィーチャーフラグを使う

| 7 min read
Author: noboru-kudo noboru-kudoの画像

アプリケーションの機能リリースでフィーチャーフラグ(またはフィーチャートグル)を使うことは結構多いかと思います。

このフィーチャーフラグの実装方法としては、単純に環境変数やパラメータで指定するものから専用のマネージドサービスを使うものまで幅広い選択肢があります。

AWSでもAWS Systems ManagerのAWS AppConfig(以下AppConfig)がこれを提供しています。
このAppConfig自体は2021年にリリースされていますが、ググってもあまり情報が見つからず、マイナーな機能と言えそうです。
とはいえ、AppConfigはアプリケーションの機能リリースをデプロイなしに安全に行えるフィーチャーフラグ機能を備えています。

少し思うところがあり、改めてAppConfigを使う方法を調べてみましたのでここで紹介します。

なお、この記事のソースコードはこちらで公開しています。

AppConfigをLambdaで使う方法(Lambda Extension)

#

ここではAWS LambdaでAppConfigのフィーチャーフラグを読んで、動的に対象機能の有効・無効を切り替えるものを想定します。

AppConfigではセッション開始(StartConfigurationSession)と最新情報取得(GetLatestConfiguration)の2つのAPIを提供しています。
フィーチャーフラグを取得する場合は、まずセッションを開始してから設定情報を取得するという流れになります(API呼び出し間はトークン引き継ぎが必要)。
また、設定が更新されない場合は最新情報取得(GetLatestConfiguration)時にデータが空になりますので、フィーチャーフラグはキャッシュしておくことも必要です。

というように使い方が少し面倒です。
ただし、Lambdaを使う場合は、この部分はLambda Extensionとして提供されていますのでこの実装を大幅に省略できます。

以下は、上記公式ドキュメントからAppConfig Lambda Extensionのアーキテクチャの抜粋です。

AppConfig Lambda Extension

Lambda Extensionが間に入ってAppConfigとのやりとりを仲介してくれている様子が分かります。

Lambdaイベントハンドラー作成

#

Lambda関数のソースコードは以下のようになりました。

import { APIGatewayProxyHandler } from 'aws-lambda';

type FeatureFlags = {
[name: string]: {
enabled: boolean;
}
}
export const sample: APIGatewayProxyHandler = async (event, context) => {
// AppConfig ExtensionからFeatureフラグ取得
const appName = 'sample-app';
const env = process.env.STAGE;
const profile = 'default';
const resp = await fetch(`http://localhost:2772/applications/${appName}/environments/${env}/configurations/${profile}`);
const flags: FeatureFlags = await resp.json();

return {
statusCode: 200,
body: JSON.stringify({
flags: {
featureA: flags.featureA.enabled,
featureB: flags.featureB.enabled
}
}),
headers: {
'Content-Type': 'application/json'
}
};
};

fetch関数を実行しているところポイントです。AWS SDKではなく、AppConfigのLambda Extensionが提供するローカルエンドポイント(http://localhost:2772)からフィーチャーフラグを取得しています。
これにより、フィーチャーフラグのローカルキャッシュや定期的(デフォルトは45秒)な最新情報の取得の実装が省略できます。

ポート番号は環境変数で変更可能ですが、ここではデフォルト値の2772をそのまま使っています。
URLのappName(AppConfigアプリケーション名)、env(デプロイメント)、profile(設定プロファイル)はAppConfig側で設定するものです。

このコードでは取得したフィーチャーフラグからfeatureAfeatureBを取得して、APIレスポンスとして返すだけのものです(実際はこれをもとに振る舞いが変わる想定です)。

AWSリソース設定(AWS CDK)

#

リソースのプロビジョニングにはAWS CDKを使います。

全体のソースコードはこちらです。
少し長いですがやっていることは単純です。

1. AWS AppConfig

#
// アプリケーション
const configApp = new appConfig.CfnApplication(this, 'AppConfigSample', {
name: 'sample-app'
});
// 環境
const devEnv = new appConfig.CfnEnvironment(this, 'AppConfigDev', {
name: 'dev',
applicationId: configApp.ref
});
// 設定プロファイル
const appConfigDefault = new appConfig.CfnConfigurationProfile(this, 'AppConfigDefaultProfile', {
name: 'default',
applicationId: configApp.ref,
type: 'AWS.AppConfig.FeatureFlags',
locationUri: 'hosted'
});

AppConfigまわりのリソースです。マイナーサービスゆえかAppConfigではL2コンストラクトがないので、L1コンストラクトで書いています。

なお、ここではAppConfigのベースのアプリケーション、環境、設定プロファイルのみを作成し、実際のフィーチャーフラグはマネジメントコンソールより管理するものと想定しています。

2. Lambda関数

#
const sampleLambda = new nodejsLambda.NodejsFunction(this, 'SampleLambda', {
entry: '../app/handler.ts',
handler: 'sample',
runtime: Runtime.NODEJS_18_X,
functionName: 'sample-function',
logRetention: RetentionDays.ONE_DAY,
environment: {
STAGE: stage,
AWS_APPCONFIG_EXTENSION_LOG_LEVEL: 'debug'
}
});
// Lambda Function URL
const url = sampleLambda.addFunctionUrl({
authType: FunctionUrlAuthType.NONE
});

シンプルなLambda関数です。

ここでのポイントはLambda Extension関係の設定をLambda関数の環境変数として指定するところくらいです(AWS_APPCONFIG_EXTENSION_XXXX)。
Lambda Extensionの詳細な設定は、以下公式ドキュメントを参照してください。

3. AppConfig Lambda Extension

#
const extensionArn = 'arn:aws:lambda:ap-northeast-1:980059726660:layer:AWS-AppConfig-Extension:84';
sampleLambda.addLayers(LayerVersion.fromLayerVersionArn(this, 'AppConfigExtension', extensionArn));
// AppConfig Extensionの実行ロール
const appConfigRole = new iam.Role(this, 'AppConfigExtensionRole', {
roleName: 'AppConfigExtensionRole',
assumedBy: sampleLambda.grantPrincipal, // 利用するLambdaで引き受け可能
inlinePolicies: {
UserTablePut: new iam.PolicyDocument({
statements: [new iam.PolicyStatement({
actions: ['appconfig:StartConfigurationSession', 'appconfig:GetLatestConfiguration'],
effect: Effect.ALLOW,
resources: [`arn:aws:appconfig:${this.region}:${this.account}:application/${configApp.ref}/environment/${devEnv.ref}/configuration/${appConfigDefault.ref}`]
})]
})
}
});
sampleLambda.addEnvironment('AWS_APPCONFIG_EXTENSION_ROLE_ARN', appConfigRole.roleArn);

まず、作成したLambda関数にAWSが提供するAppConfigのLambda ExtensionをLambdaレイヤーとして追加しています。
ここで指定しているARNは以下公式ドキュメントに記載されているものです。

次に、Lambda Extensionが実行するIAMロールを作成しています。
ここではappconfig:StartConfigurationSessionappconfig:GetLatestConfigurationのみを許可しておけば問題ありません。
このロールのARNはLambda関数の環境変数(AWS_APPCONFIG_EXTENSION_ROLE_ARN)に指定します。
Lambda ExtensionはこのロールをAppConfigのフィーチャーフラグ取得時に使います。

フィーチャーフラグの動作確認

#

あとはデプロイして確認してみます。

cdk deploy --context stage=dev

デプロイ後にマネジメントコンソールからAppConfigのフィーチャーフラグを設定します。
CDKの実行でアプリケーション(sample-app)や設定プロファイル(default)、環境(dev)は作成済みです。フィーチャーフラグを追加するだけです。
defaultプロファイルに以下2つのフィーチャーフラグ(featureA/featureB)を追加しました。

AppConfig FeatureFlag1

初期状態では両機能とも無効化されています。
これをdev環境にデプロイします(アプリのデプロイではありません)。

AppConfig Deploy1

デプロイ戦略はAppConfig.AllAtOnceを選択します。これはデプロイするとすぐに設定が変わります。
他に徐々にデプロイするカナリアデプロイの戦略も指定できますし、カスタムのデプロイ戦略も作成可能です。
有効にするフィーチャーフラグの特性に応じた最適なものを選択していくことになるかと思います。
デプロイ戦略の詳細は、以下公式ドキュメントを参照してください。

デプロイが完了したら、Lambda Function URLのエンドポイントにアクセスしてみます(CDKのOutputとしてコンソールに表示されます)。

curl v1

AppConfigのフィーチャーフラグが取得できていると確認できました。

A機能(featureA)を有効にします。

AppConfig FeatureFlag2

こちらはバージョン2です。再度これをデプロイします。

AppConfig Deploy2

こちらもデプロイ後すぐに設定は反映されます。

curl v2

A機能(featureA)が有効になっていることが分かります。

最後に

#

Lambda Extensionを使うことで、セッション管理やポーリング/キャッシュといった実装を省略して簡単にフィーチャーフラグ機能を実装できました。
AppConfigを使えばフィーチャーフラグの有効化にデプロイは不要ですし、バージョン切り戻しもワンクリックです。
カナリアデプロイのような高度なデプロイ戦略も使えますし、使える機会があれば導入してみたいものですね。

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  2. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  3. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  4. Backstageで開発者ポータルサイトを構築する - 導入編 (2022-04-29)
  5. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  6. Viteベースの高速テスティングフレームワークVitestを使ってみる (2022-12-28)
  7. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  8. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  9. GitHub Actions - 構成変数(環境変数)が外部設定できるようになったので用途を整理する (2023-01-16)
  10. Jest再入門 - 関数・モジュールモック編 (2022-07-03)