AWS AppConfigのFeatureFlagをCDKでデプロイする

2025/03/01に公開

目的

FeatureFlagを試したくなったので、AWS AppConfigを使ってやってみます。
この記事ではCDKで作るところまでで、利用するのはまた別の記事でやります。

AWS AppConfigの構成

AWS AppConfigにはいくつかの構成要素があります。

アプリケーション

まず大本になるアプリケーションがあります。名前が設定できて、作成するとアプリケーションIDが発行されます。

設定プロファイル

アプリケーション下に作られるFeatureFlagの履歴を管理する要素です。ここでFeatureFlagを設定して各環境にデプロイされます。現状複数つくるメリットがよくわからないのでdefaultという名前で1つだけ作っています。

環境

FeatureFlagをデプロイする環境です。一般的にはstaging, productionのようになります。

デプロイ戦略

環境にデプロイしたときの反映の速度を制御できます。実験などでやるときは早くやりたいので、速攻で終わらせる戦略を作りました。

CDK

bin

bin/cdk.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkApplicationStack } from '../lib/cdk-application-stack';
import { CdkConfigurationStack } from '../lib/cdk-configuration-stack';
import { CdkLambdaStack } from '../lib/cdk-lambda-stack';
import { getCdkEnv, getConfiguration, getProcejctName, getEnvironment } from '../lib/util';

const app = new cdk.App();

// REALM=staging CONFIGURATION=default npm run cdk deploy feature-flag-sample-project-init
new CdkApplicationStack(app, `${getProcejctName()}-project-init`, {
  env: getCdkEnv()
});

// REALM=staging CONFIGURATION=default npm run cdk deploy feature-flag-sample-staging-default-configuration
new CdkConfigurationStack(app, `${getProcejctName()}-${getEnvironment()}-${getConfiguration()}-configuration`, {
  env: getCdkEnv()
});

アプリケーション、環境、戦略

lib/cdk-application-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as appConfig from 'aws-cdk-lib/aws-appconfig';
import { getApplicationReferenceArn, getApplicationReferenceId, getEnvironmentReferenceId, getProcejctName, getEnvironmentArray, getStrategyReferenceArn } from './util';


export class CdkApplicationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // アプリケーション
    const application = new appConfig.Application(this, 'AppConfigSample', {
      applicationName: getProcejctName(),
    })

    // ARNを出力
    new cdk.CfnOutput(this, 'configAppOutputArn', {
      exportName: getApplicationReferenceArn(),
      description: 'AppConfig Application ARN',
      value: application.applicationArn
    })

    // IDを出力
    new cdk.CfnOutput(this, 'configAppOutputId', {
      exportName: getApplicationReferenceId(),
      description: 'AppConfig Application Id',
      value: application.applicationId
    })

    // 環境
    const environmentArray = getEnvironmentArray()
    for (let index in environmentArray) {
      const name = environmentArray[index];
      const env = new appConfig.Environment(this, `AppConfigEnvironment${name}`, {
        environmentName: name,
        application,
      });
      // Idを出力
      new cdk.CfnOutput(this, `environmentOutput${name}`, {
        exportName: getEnvironmentReferenceId(name),
        description: `AppConfig Environment ID ${name}`,
        value: env.environmentId
      })
    }

    // 戦略
    const strategy = new appConfig.DeploymentStrategy(this, "DeploymentStrategy", {
      rolloutStrategy: appConfig.RolloutStrategy.linear({
        deploymentDuration: cdk.Duration.minutes(0),
        growthFactor: 100,
        finalBakeTime: cdk.Duration.minutes(0),
      }),
      deploymentStrategyName: "FastStrategy"
    })

    // ARNを出力
    new cdk.CfnOutput(this, 'DeploymentStrategyOutput', {
      exportName: getStrategyReferenceArn(),
      description: 'AppConfig Strategy ARN',
      value: strategy.deploymentStrategyArn,
    })
  }
}

設定プロファイル、デプロイ

lib/cdk-configuration-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as appConfig from 'aws-cdk-lib/aws-appconfig';
import { getApplicationReferenceArn, getConfiguration, getConfigureReferenceId, getEnvironmentReferenceId, getEnvironment, getStrategyReferenceArn } from './util';

export class CdkConfigurationStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // アプリケーション
    const application = appConfig.Application.fromApplicationArn(
      this, 'AppConfigSample', 
      cdk.Fn.importValue(getApplicationReferenceArn()));

    // 環境
    const environment = appConfig.Environment.fromEnvironmentAttributes(
      this, 'AppConfigEnvironment', 
      {

        environmentId: cdk.Fn.importValue(getEnvironmentReferenceId(getEnvironment())),
        application,
        name: getEnvironment(),
      }
    );

    // 戦略
    const strategy = appConfig.DeploymentStrategy.fromDeploymentStrategyArn(
      this, 'DeploymentStrategy', cdk.Fn.importValue(getStrategyReferenceArn()));

    // プロファイル
    const configure = new appConfig.HostedConfiguration(this, 'AppConfigProfile', {
      application,
      type: appConfig.ConfigurationType.FEATURE_FLAGS,
      name: getConfiguration(),
      deployTo: [environment],
      deploymentStrategy: strategy,
      content: appConfig.ConfigurationContent.fromFile(`./feature_flags/${getEnvironment()}_${getConfiguration()}.json`),
    });

    // Idを出力
    new cdk.CfnOutput(this, 'DeploymentStrategyOutput', {
        exportName: getConfigureReferenceId(),
        description: 'AppConfig Configure ID',
        value: configure.configurationProfileId
      })
  }
}

Utility

lib/util.ts
import * as radash from 'radash';

export type Environment = 'staging' | 'production'

export function getEnvironment() : Environment {
    return process.env['ENVIRONMENT'] as Environment
}

export function getEnvironmentArray() : Environment[] {
    return ['staging', 'production']
}

export function getProcejctName() : string {
    return 'feature-flag-sample'
}

export function getProcejctNameOnlyAlphanumeric() : string {
    return 'FeatureFlagSample'
}

export function getCdkEnv() {
    return { 
        account: process.env['ACCOUNT'], 
        region: 'ap-northeast-1' 
    }
}

export type Configuration = 'default'

export function getConfiguration() : Configuration {
    return process.env['CONFIGURATION'] as Configuration
}

export function getApplicationReferenceArn() : string {
    return `${getProcejctNameOnlyAlphanumeric()}AppconfigApplicationArn`
}

export function getApplicationReferenceId() : string {
    return `${getProcejctNameOnlyAlphanumeric()}AppconfigApplicationId`
}

export function getStrategyReferenceArn() : string {
    return `${getProcejctNameOnlyAlphanumeric()}FastStrategyArn`
}

export function getConfigureReferenceId() : string {
    return `${getProcejctNameOnlyAlphanumeric()}${radash.capitalize(getConfiguration())}ConfigureId`
}

export function getEnvironmentReferenceId(environment: Environment) : string {
    return `${getProcejctNameOnlyAlphanumeric()}AppconfigEnvironmentId${radash.capitalize(environment)}`
}

export const makeEnvironmentResourceName = (name: string): string => {
    return `${getProcejctName()}-${getEnvironment()}-${name}`
}

FeatureFlag

feature_flags/stanig_default.json
{
    "version": "1",
    "flags": {
        "feature_abc": {
          "name": "featureABC"
        },
        "feature_efg": {
          "name": "featureEFG"
        }
      },
      "values": {
        "feature_abc": {
          "enabled": true
        },
        "feature_efg": {
          "enabled": false
        }
      }
}

実行

cdk bootstrap
ENVIRONMENT=staging CONFIGURATION=default npm run cdk deploy feature-flag-sample-project-init
ENVIRONMENT=staging CONFIGURATION=default npm run cdk deploy feature-flag-sample-staging-default-configuration

まとめ

本来の設定プロファイルは履歴を管理する仕組みなんですが、FeatureFlagをgitで管理したいので、環境名と設定プロファイル名でjsonとして保存しています。
本来の履歴っぽい使い方がいいのか、よくわからないですが、今後色々試していこうと思います。

Discussion