⛳
AWS AppConfigのFeatureFlagをCDKでデプロイする
目的
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