Skip to content

Commit

Permalink
fix(autoscaling): can't configure notificationTypes (aws#8294)
Browse files Browse the repository at this point in the history
----
AutoScalingGroup [notificationconfigurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations) property allows configuring autoscaling to send notifications about fleet scaling events to one or more SNS topics. 

The current AutoScalingGroup API expose a `notificationsTopic` property which only allows configuring a single topic, and does not allows configuring which events will trigger a notification but instead configures all notifications, which can be rather noisy.

This PR deprecates the `notificationsTopic` property and introduce a `notifications` property
 which allows configuring multiple `NotificationConfiguration`, each with is own SNS topic and a custom list of events which will trigger a notification.

closes aws#8053

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
NetaNir authored Jun 10, 2020
1 parent 24e474b commit 01ef1ca
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 11 deletions.
129 changes: 118 additions & 11 deletions packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,17 @@ export interface CommonAutoScalingGroupProps {
* SNS topic to send notifications about fleet changes
*
* @default - No fleet change notifications will be sent.
* @deprecated use `notifications`
*/
readonly notificationsTopic?: sns.ITopic;

/**
* Configure autoscaling group to send notifications about fleet changes to an SNS topic(s)
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations
* @default - No fleet change notifications will be sent.
*/
readonly notifications?: NotificationConfiguration[];

/**
* Whether the instances can initiate connections to anywhere by default
*
Expand Down Expand Up @@ -435,6 +443,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
private readonly securityGroups: ec2.ISecurityGroup[] = [];
private readonly loadBalancerNames: string[] = [];
private readonly targetGroupArns: string[] = [];
private readonly notifications: NotificationConfiguration[] = [];

constructor(scope: Construct, id: string, props: AutoScalingGroupProps) {
super(scope, id);
Expand Down Expand Up @@ -513,6 +522,23 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
throw new Error('maxInstanceLifetime must be between 7 and 365 days (inclusive)');
}

if (props.notificationsTopic && props.notifications) {
throw new Error('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead');
}

if (props.notificationsTopic) {
this.notifications = [{
topic: props.notificationsTopic,
}];
}

if (props.notifications) {
this.notifications = props.notifications.map(nc => ({
topic: nc.topic,
scalingEvents: nc.scalingEvents ?? ScalingEvents.ALL,
}));
}

const { subnetIds, hasPublic } = props.vpc.selectSubnets(props.vpcSubnets);
const asgProps: CfnAutoScalingGroupProps = {
cooldown: props.cooldown !== undefined ? props.cooldown.toSeconds().toString() : undefined,
Expand All @@ -522,17 +548,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
launchConfigurationName: launchConfig.ref,
loadBalancerNames: Lazy.listValue({ produce: () => this.loadBalancerNames }, { omitEmpty: true }),
targetGroupArns: Lazy.listValue({ produce: () => this.targetGroupArns }, { omitEmpty: true }),
notificationConfigurations: !props.notificationsTopic ? undefined : [
{
topicArn: props.notificationsTopic.topicArn,
notificationTypes: [
'autoscaling:EC2_INSTANCE_LAUNCH',
'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',
'autoscaling:EC2_INSTANCE_TERMINATE',
'autoscaling:EC2_INSTANCE_TERMINATE_ERROR',
],
},
],
notificationConfigurations: this.renderNotificationConfiguration(),
vpcZoneIdentifier: subnetIds,
healthCheckType: props.healthCheck && props.healthCheck.type,
healthCheckGracePeriod: props.healthCheck && props.healthCheck.gracePeriod && props.healthCheck.gracePeriod.toSeconds(),
Expand Down Expand Up @@ -667,6 +683,17 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
};
}
}

private renderNotificationConfiguration(): CfnAutoScalingGroup.NotificationConfigurationProperty[] | undefined {
if (this.notifications.length === 0) {
return undefined;
}

return this.notifications.map(notification => ({
topicArn: notification.topic.topicArn,
notificationTypes: notification.scalingEvents ? notification.scalingEvents._types : ScalingEvents.ALL._types,
}));
}
}

/**
Expand All @@ -691,6 +718,53 @@ export enum UpdateType {
ROLLING_UPDATE = 'RollingUpdate',
}

/**
* AutoScalingGroup fleet change notifications configurations.
* You can configure AutoScaling to send an SNS notification whenever your Auto Scaling group scales.
*/
export interface NotificationConfiguration {
/**
* SNS topic to send notifications about fleet scaling events
*/
readonly topic: sns.ITopic;

/**
* Which fleet scaling events triggers a notification
* @default ScalingEvents.ALL
*/
readonly scalingEvents?: ScalingEvents;
}

/**
* Fleet scaling events
*/
export enum ScalingEvent {
/**
* Notify when an instance was launced
*/
INSTANCE_LAUNCH = 'autoscaling:EC2_INSTANCE_LAUNCH',

/**
* Notify when an instance was terminated
*/
INSTANCE_TERMINATE = 'autoscaling:EC2_INSTANCE_TERMINATE',

/**
* Notify when an instance failed to terminate
*/
INSTANCE_TERMINATE_ERROR = 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR',

/**
* Notify when an instance failed to launch
*/
INSTANCE_LAUNCH_ERROR = 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',

/**
* Send a test notification to the topic
*/
TEST_NOTIFICATION = 'autoscaling:TEST_NOTIFICATION'
}

/**
* Additional settings when a rolling update is selected
*/
Expand Down Expand Up @@ -766,6 +840,39 @@ export interface RollingUpdateConfiguration {
readonly suspendProcesses?: ScalingProcess[];
}

/**
* A list of ScalingEvents, you can use one of the predefined lists, such as ScalingEvents.ERRORS
* or create a custome group by instantiating a `NotificationTypes` object, e.g: `new NotificationTypes(`NotificationType.INSTANCE_LAUNCH`)`.
*/
export class ScalingEvents {
/**
* Fleet scaling errors
*/
public static readonly ERRORS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH_ERROR, ScalingEvent.INSTANCE_TERMINATE_ERROR);

/**
* All fleet scaling events
*/
public static readonly ALL = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH,
ScalingEvent.INSTANCE_LAUNCH_ERROR,
ScalingEvent.INSTANCE_TERMINATE,
ScalingEvent.INSTANCE_TERMINATE_ERROR);

/**
* Fleet scaling launch events
*/
public static readonly LAUNCH_EVENTS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, ScalingEvent.INSTANCE_LAUNCH_ERROR);

/**
* @internal
*/
public readonly _types: ScalingEvent[];

constructor(...types: ScalingEvent[]) {
this._types = types;
}
}

export enum ScalingProcess {
LAUNCH = 'Launch',
TERMINATE = 'Terminate',
Expand Down
139 changes: 139 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ABSENT, expect, haveResource, haveResourceLike, InspectionFailure, Reso
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
Expand Down Expand Up @@ -1026,6 +1027,144 @@ export = {
test.done();
},

'throw if notification and notificationsTopics are both configured'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
const topic = new sns.Topic(stack, 'MyTopic');

// THEN
test.throws(() => {
new autoscaling.AutoScalingGroup(stack, 'MyASG', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
notificationsTopic: topic,
notifications: [{
topic,
}],
});
}, 'Can not set notificationsTopic and notifications, notificationsTopic is deprected use notifications instead');
test.done();
},

'allow configuring notifications'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
const topic = new sns.Topic(stack, 'MyTopic');

// WHEN
new autoscaling.AutoScalingGroup(stack, 'MyASG', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
notifications: [
{
topic,
scalingEvents: autoscaling.ScalingEvents.ERRORS,
},
{
topic,
scalingEvents: new autoscaling.ScalingEvents(autoscaling.ScalingEvent.INSTANCE_TERMINATE),
},
],
});

// THEN
expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', {
NotificationConfigurations : [
{
TopicARN : { Ref : 'MyTopic86869434' },
NotificationTypes : [
'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',
'autoscaling:EC2_INSTANCE_TERMINATE_ERROR',
],
},
{
TopicARN : { Ref : 'MyTopic86869434' },
NotificationTypes : [
'autoscaling:EC2_INSTANCE_TERMINATE',
],
},
]},
));

test.done();
},

'notificationTypes default includes all non test NotificationType'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
const topic = new sns.Topic(stack, 'MyTopic');

// WHEN
new autoscaling.AutoScalingGroup(stack, 'MyASG', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
notifications: [
{
topic,
},
],
});

// THEN
expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', {
NotificationConfigurations : [
{
TopicARN : { Ref : 'MyTopic86869434' },
NotificationTypes : [
'autoscaling:EC2_INSTANCE_LAUNCH',
'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',
'autoscaling:EC2_INSTANCE_TERMINATE',
'autoscaling:EC2_INSTANCE_TERMINATE_ERROR',
],
},
]},
));

test.done();
},

'setting notificationTopic configures all non test NotificationType'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
const topic = new sns.Topic(stack, 'MyTopic');

// WHEN
new autoscaling.AutoScalingGroup(stack, 'MyASG', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
notificationsTopic: topic,
});

// THEN
expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', {
NotificationConfigurations : [
{
TopicARN : { Ref : 'MyTopic86869434' },
NotificationTypes : [
'autoscaling:EC2_INSTANCE_LAUNCH',
'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',
'autoscaling:EC2_INSTANCE_TERMINATE',
'autoscaling:EC2_INSTANCE_TERMINATE_ERROR',
],
},
]},
));

test.done();
},

'NotificationTypes.ALL includes all non test NotificationType'(test: Test) {
test.deepEqual(Object.values(autoscaling.ScalingEvent).length - 1, autoscaling.ScalingEvents.ALL._types.length);
test.done();
},
};

function mockVpc(stack: cdk.Stack) {
Expand Down

0 comments on commit 01ef1ca

Please sign in to comment.