Skip to content

Commit

Permalink
feat(appconfig): Constrain environments to a single deployment at a time
Browse files Browse the repository at this point in the history
[Issue aws#29345]
------------------------------------------------------------------------

[Reason for this change]
------------------------------------------------------------------------
The current L2 AppConfig constructs do not have any guardrails that
prevent simultaneous Deployments to a single Environment.
This is not allowed, and will result in Cfn deploy-time conflicts.

[Description of changes]
------------------------------------------------------------------------
This commit adds a pair of new public methods to IEnvironment that
enable the addition of a new Deployment for a given IConfiguration.

It then updates the creation of new Deployments in ConfigurationBase
to utilize these new methods instead of the current resource creation.

These new methods interact with an internal queue.
This queue creates a chain of Cfn dependencies between Deployments
in order to enforce that only a single Deployment can be in progress
for the Environment at any given time.

[Description of how you validated changes]
------------------------------------------------------------------------
Added new unit test coverage.
  • Loading branch information
M-Hawkins committed Mar 15, 2024
1 parent fa3feea commit 557a404
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 13 deletions.
13 changes: 2 additions & 11 deletions packages/aws-cdk-lib/aws-appconfig/lib/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
import * as fs from 'fs';
import * as path from 'path';
import { Construct, IConstruct } from 'constructs';
import { CfnConfigurationProfile, CfnDeployment, CfnHostedConfigurationVersion } from './appconfig.generated';
import { CfnConfigurationProfile, CfnHostedConfigurationVersion } from './appconfig.generated';
import { IApplication } from './application';
import { DeploymentStrategy, IDeploymentStrategy, RolloutStrategy } from './deployment-strategy';
import { IEnvironment } from './environment';
import { ActionPoint, IEventDestination, ExtensionOptions, IExtension, IExtensible, ExtensibleBase } from './extension';
import { getHash } from './private/hash';
import * as cp from '../../aws-codepipeline';
import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
Expand Down Expand Up @@ -319,15 +318,7 @@ abstract class ConfigurationBase extends Construct implements IConfiguration, IE
if ((this.deployTo && !this.deployTo.includes(environment))) {
return;
}
new CfnDeployment(this, `Deployment${getHash(environment.name!)}`, {
applicationId: this.application.applicationId,
configurationProfileId: this.configurationProfileId,
deploymentStrategyId: this.deploymentStrategy!.deploymentStrategyId,
environmentId: environment.environmentId,
configurationVersion: this.versionNumber!,
description: this.description,
kmsKeyIdentifier: this.deploymentKey?.keyArn,
});
environment.addDeployment(this);
});
}
}
Expand Down
45 changes: 44 additions & 1 deletion packages/aws-cdk-lib/aws-appconfig/lib/environment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Construct } from 'constructs';
import { CfnEnvironment } from './appconfig.generated';
import { CfnDeployment, CfnEnvironment } from './appconfig.generated';
import { IApplication } from './application';
import { IConfiguration } from './configuration';
import { ActionPoint, IEventDestination, ExtensionOptions, IExtension, IExtensible, ExtensibleBase } from './extension';
import { getHash } from './private/hash';
import * as cloudwatch from '../../aws-cloudwatch';
Expand Down Expand Up @@ -47,7 +48,31 @@ abstract class EnvironmentBase extends Resource implements IEnvironment, IExtens
public abstract applicationId: string;
public abstract environmentId: string;
public abstract environmentArn: string;
public abstract name?: string | undefined;
protected extensible!: ExtensibleBase;
protected deploymentQueue: Array<CfnDeployment> = [];

public addDeployment(configuration: IConfiguration): void {
const queueSize = this.deploymentQueue.push(
new CfnDeployment(configuration, `Deployment${getHash(this.name!)}`, {
applicationId: configuration.application.applicationId,
configurationProfileId: configuration.configurationProfileId,
deploymentStrategyId: configuration.deploymentStrategy!.deploymentStrategyId,
environmentId: this.environmentId,
configurationVersion: configuration.versionNumber!,
description: configuration.description,
kmsKeyIdentifier: configuration.deploymentKey?.keyArn,
}),
);

if (queueSize > 1) {
this.deploymentQueue[queueSize - 1].addDependency(this.deploymentQueue[queueSize - 2]);
}
}

public addDeployments(...configurations: IConfiguration[]): void {
configurations.forEach((config) => this.addDeployment(config));
}

public on(actionPoint: ActionPoint, eventDestination: IEventDestination, options?: ExtensionOptions) {
this.extensible.on(actionPoint, eventDestination, options);
Expand Down Expand Up @@ -154,6 +179,7 @@ export class Environment extends EnvironmentBase {
public readonly applicationId = applicationId;
public readonly environmentId = environmentId;
public readonly environmentArn = environmentArn;
public readonly name?: string | undefined;
}

return new Import(scope, id, {
Expand Down Expand Up @@ -413,6 +439,23 @@ export interface IEnvironment extends IResource {
*/
readonly environmentArn: string;

/**
* Creates a deployment of the supplied configuration to this environment.
* Note that you can only deploy one configuration at a time to an environment.
* However, you can deploy one configuration each to different environments at the same time.
* If more than one deployment is requested for this environment, they will occur in the same order they were provided.
*
* @param configuration The configuration that will be deployed to this environment.
*/
addDeployment(configuration: IConfiguration): void;

/**
* Creates a deployment for each of the supplied configurations to this environment.
*
* @param configurations The configurations that will be deployed to this environment.
*/
addDeployments(...configurations: Array<IConfiguration>): void;

/**
* Adds an extension defined by the action point and event destination and also
* creates an extension association to the environment.
Expand Down
212 changes: 211 additions & 1 deletion packages/aws-cdk-lib/aws-appconfig/test/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Template } from '../../assertions';
import { Alarm, CompositeAlarm, Metric } from '../../aws-cloudwatch';
import * as iam from '../../aws-iam';
import * as cdk from '../../core';
import { Application, Environment, Monitor } from '../lib';
import { Application, ConfigurationContent, Environment, HostedConfiguration, Monitor } from '../lib';

describe('environment', () => {
test('default environment', () => {
Expand Down Expand Up @@ -54,6 +54,216 @@ describe('environment', () => {
});
});

test('environment with single deployment', () => {
const stack = new cdk.Stack();
const application = new Application(stack, 'MyAppConfig');
const env = new Environment(stack, 'MyEnvironment', {
application,
});

const firstConfig = new HostedConfiguration(stack, 'FirstConfig', {
application,
content: ConfigurationContent.fromInlineText('This is my content 1'),
});
env.addDeployment(firstConfig);

const actual = Template.fromStack(stack);

actual.hasResourceProperties('AWS::AppConfig::Environment', {
Name: 'MyEnvironment',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
});

actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', {
Name: 'FirstConfig',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
LocationUri: 'hosted',
});
actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
ConfigurationProfileId: {
Ref: 'FirstConfigConfigurationProfileDEF37C63',
},
Content: 'This is my content 1',
ContentType: 'text/plain',
});
actual.hasResource('AWS::AppConfig::Deployment', {
Properties: {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
EnvironmentId: {
Ref: 'MyEnvironment465E4DEA',
},
ConfigurationVersion: {
Ref: 'FirstConfigC35E996C',
},
ConfigurationProfileId: {
Ref: 'FirstConfigConfigurationProfileDEF37C63',
},
DeploymentStrategyId: {
Ref: 'FirstConfigDeploymentStrategy863BBA9A',
},
},
});

actual.resourceCountIs('AWS::AppConfig::Deployment', 1);
});

test('environment with multiple deployments', () => {
const stack = new cdk.Stack();
const application = new Application(stack, 'MyAppConfig');
const env = new Environment(stack, 'MyEnvironment', {
application,
});

const firstConfig = new HostedConfiguration(stack, 'FirstConfig', {
application,
content: ConfigurationContent.fromInlineText('This is my content 1'),
});
const secondConfig = new HostedConfiguration(stack, 'SecondConfig', {
application,
content: ConfigurationContent.fromInlineText('This is my content 2'),
});
const thirdConfig = new HostedConfiguration(stack, 'ThirdConfig', {
application,
content: ConfigurationContent.fromInlineText('This is my content 3'),
});

env.addDeployments(firstConfig, secondConfig);
env.addDeployment(thirdConfig);

const actual = Template.fromStack(stack);

actual.hasResourceProperties('AWS::AppConfig::Environment', {
Name: 'MyEnvironment',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
});

actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', {
Name: 'FirstConfig',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
LocationUri: 'hosted',
});
actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
ConfigurationProfileId: {
Ref: 'FirstConfigConfigurationProfileDEF37C63',
},
Content: 'This is my content 1',
ContentType: 'text/plain',
});
actual.hasResource('AWS::AppConfig::Deployment', {
Properties: {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
EnvironmentId: {
Ref: 'MyEnvironment465E4DEA',
},
ConfigurationVersion: {
Ref: 'FirstConfigC35E996C',
},
ConfigurationProfileId: {
Ref: 'FirstConfigConfigurationProfileDEF37C63',
},
DeploymentStrategyId: {
Ref: 'FirstConfigDeploymentStrategy863BBA9A',
},
},
});

actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', {
Name: 'SecondConfig',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
LocationUri: 'hosted',
});
actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
ConfigurationProfileId: {
Ref: 'SecondConfigConfigurationProfileE64FE7B4',
},
Content: 'This is my content 2',
ContentType: 'text/plain',
});
actual.hasResource('AWS::AppConfig::Deployment', {
Properties: {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
EnvironmentId: {
Ref: 'MyEnvironment465E4DEA',
},
ConfigurationVersion: {
Ref: 'SecondConfig22E40AAE',
},
ConfigurationProfileId: {
Ref: 'SecondConfigConfigurationProfileE64FE7B4',
},
DeploymentStrategyId: {
Ref: 'SecondConfigDeploymentStrategy9929738B',
},
},
DependsOn: ['FirstConfigDeployment52928BE68587B'],
});

actual.hasResourceProperties('AWS::AppConfig::ConfigurationProfile', {
Name: 'ThirdConfig',
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
LocationUri: 'hosted',
});
actual.hasResourceProperties('AWS::AppConfig::HostedConfigurationVersion', {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
ConfigurationProfileId: {
Ref: 'ThirdConfigConfigurationProfile4945C970',
},
Content: 'This is my content 3',
ContentType: 'text/plain',
});
actual.hasResource('AWS::AppConfig::Deployment', {
Properties: {
ApplicationId: {
Ref: 'MyAppConfigB4B63E75',
},
EnvironmentId: {
Ref: 'MyEnvironment465E4DEA',
},
ConfigurationVersion: {
Ref: 'ThirdConfig498595D6',
},
ConfigurationProfileId: {
Ref: 'ThirdConfigConfigurationProfile4945C970',
},
DeploymentStrategyId: {
Ref: 'ThirdConfigDeploymentStrategy246FBD1A',
},
},
DependsOn: ['SecondConfigDeployment5292843F35B55'],
});

actual.resourceCountIs('AWS::AppConfig::Deployment', 3);
});

test('environment with monitors with alarm and alarmRole', () => {
const stack = new cdk.Stack();
const app = new Application(stack, 'MyAppConfig');
Expand Down

0 comments on commit 557a404

Please sign in to comment.