Skip to content

Commit

Permalink
feat(aws-ec2): AutoScalingGroup rolling updates
Browse files Browse the repository at this point in the history
Fixes #278.
  • Loading branch information
Rico Huijbers committed Aug 17, 2018
1 parent 5164365 commit 2864b98
Showing 1 changed file with 239 additions and 0 deletions.
239 changes: 239 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/auto-scaling-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,47 @@ export interface AutoScalingGroupProps {
* @default true
*/
allowAllOutbound?: boolean;

/**
* What to do when an AutoScalingGroup's instance configuration is changed
*
* This is applied when any of the settings on the ASG are changed that
* affect how the instances should be created (VPC, instance type, startup
* scripts, etc.). It indicates how the existing instances should be
* replaced with new instances matching the new config. By default, nothing
* is done and only new instances are launched with the new config.
*
* @default UpdateType.None
*/
updateType?: UpdateType;

/**
* Configuration for rolling updates
*
* Only used if updateType == UpdateType.RollingUpdate.
*/
rollingUpdateConfiguration?: RollingUpdateConfiguration;

/**
* Configuration for replacing updates.
*
* Only used if updateType == UpdateType.ReplacingUpdate. Specifies how

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

Is this required for ReplacingUpdate? If it's not specified, is there a sensible default that we can use?

This comment has been minimized.

Copy link
@rix0rrr

rix0rrr Aug 28, 2018

Contributor

Ah yeah, good point, forgot to add a @default marker.

Going by the CloudFormation docs, when not supplied the default is 100.

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

If this is set and updateType is not set, what happens? I would expect udpateType to automatically be set to replacing update I guess

This comment has been minimized.

Copy link
@rix0rrr

rix0rrr Aug 28, 2018

Contributor

Right now, it's ignored. I don't know if I share your expectation, I think I would actually expect an error if anything.

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

Ignored is definitely not what we want - error is fine, but why be nasty to the user? If they set this property and not rollingUpdateConfiguration, they probably wanted replacing update...

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

We can also consider an alternative API that will make this structurally impossible. Maybe something polymorphic:

{
    updateType: AutoScalingUpdate.replacing({ minSuccessfulInstancePercent: 40 }),
    // or
    updateType: AutoScalingUpdate.rolling({ /* config props */ }),
}
* many instances must signal success for the update to succeed.
*/
replacingUpdateMinSuccessfulInstancesPercent?: number;

/**
* If the ASG has scheduled actions, don't reset unchanged group sizes

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

This might need a bit of elaboration. Maybe something like "Since the size of the auto-scaling group may change by scheduled actions triggered by autoscaling events, this option determines how CloudFormation behaves when ASGs are updated but their sizes have been changed by actions"... something like that

This comment has been minimized.

Copy link
@rix0rrr

rix0rrr Aug 28, 2018

Contributor

Not in the caption though, that's way too long for that space

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

Yeah, in the body...

*
* Only used if the ASG has scheduled actions (which may scale your ASG up
* or down regardless of cdk deployments). If true, the size of the group
* will only be reset if it has been changed in the CDK app. If false, the
* sizes will always be changed back to what they were in the CDK app
* on deployment.
*
* @default true
*/
ignoreUnmodifiedSizeProperties?: boolean;
}

/**
Expand Down Expand Up @@ -167,6 +208,8 @@ export class AutoScalingGroup extends cdk.Construct implements IClassicLoadBalan

this.autoScalingGroup = new autoscaling.cloudformation.AutoScalingGroupResource(this, 'ASG', asgProps);
this.osType = machineImage.os.type;

this.applyUpdatePolicies(props);
}

public attachToClassicLB(loadBalancer: ClassicLoadBalancer): void {
Expand All @@ -191,4 +234,200 @@ export class AutoScalingGroup extends cdk.Construct implements IClassicLoadBalan
public addToRolePolicy(statement: cdk.PolicyStatement) {
this.role.addToPolicy(statement);
}

/**
* Apply CloudFormation update policies for the AutoScalingGroup
*/
private applyUpdatePolicies(props: AutoScalingGroupProps) {
if (props.updateType === UpdateType.ReplacingUpdate) {
this.asgUpdatePolicy.autoScalingReplacingUpdate = { willReplace: true };

if (props.replacingUpdateMinSuccessfulInstancesPercent !== undefined) {
if (this.autoScalingGroup.options.creationPolicy === undefined) {
this.autoScalingGroup.options.creationPolicy = {};
}

// Yes, this goes on CreationPolicy, not as a process parameter to ReplacingUpdate.
// It's a little confusing, but the docs seem to explicitly state it will only be used
// during the update?
//
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html
this.autoScalingGroup.options.creationPolicy.autoScalingCreationPolicy = {
minSuccessfulInstancesPercent: validatePercentage(props.replacingUpdateMinSuccessfulInstancesPercent)
};
}
} else if (props.updateType === UpdateType.RollingUpdate) {
this.asgUpdatePolicy.autoScalingRollingUpdate = renderRollingUpdateConfig(props.rollingUpdateConfiguration);
}

// undefined is treated as 'true'
if (props.ignoreUnmodifiedSizeProperties !== false) {
this.asgUpdatePolicy.autoScalingScheduledAction = { ignoreUnmodifiedGroupSizeProperties: true };
}
}

/**
* Create the ASG update policy if not set yet and return a reference to it
*/
private get asgUpdatePolicy() {
if (this.autoScalingGroup.options.updatePolicy === undefined) {
this.autoScalingGroup.options.updatePolicy = {};
}
return this.autoScalingGroup.options.updatePolicy;
}
}

/**
* The type of update to perform on instances in this AutoScalingGroup
*/
export enum UpdateType {
/**
* Don't do anything

This comment has been minimized.

Copy link
@eladb

eladb Aug 28, 2018

Contributor

... which means that only EC2 instances started after the update will have the new ASG configuration

*/
None = 'None',

/**
* Replace the entire AutoScalingGroup
*
* Builds a new AutoScalingGroup first, then delete the old one.
*/
ReplacingUpdate = 'Replace',

/**
* Replace the instances in the AutoScalingGroup.
*/
RollingUpdate = 'RollingUpdate',
}

/**
* Additional settings when a rolling update is selected
*/
export interface RollingUpdateConfiguration {
/**
* The maximum number of instances that AWS CloudFormation updates at once.
*
* @default 1
*/
maxBatchSize?: number;

/**
* The minimum number of instances that must be in service before more instances are replaced.
*
* This number affects the speed of the replacement.
*
* @default 0
*/
minInstancesInService?: number;

/**
* The percentage of instances that must signal success for an update to succeed.
*
* If an instance doesn't send a signal within the time specified in the
* pauseTime property, AWS CloudFormation assumes that the instance wasn't
* updated.
*
* This number affects the success of the replacement.
*
* If you specify this property, you must also enable the
* waitOnResourceSignals and pauseTime properties.
*
* @default 100
*/
minSuccessfulInstancesPercent?: number;

/**
* The pause time after making a change to a batch of instances.
*
* This is intended to give those instances time to start software applications.
*
* Specify PauseTime in the ISO8601 duration format (in the format
* PT#H#M#S, where each # is the number of hours, minutes, and seconds,
* respectively). The maximum PauseTime is one hour (PT1H).
*
* @default 300 if the waitOnResourceSignals property is true, otherwise 0
*/
pauseTimeSec?: number;

/**
* Specifies whether the Auto Scaling group waits on signals from new instances during an update.
*
* AWS CloudFormation must receive a signal from each new instance within
* the specified PauseTime before continuing the update.
*
* To have instances wait for an Elastic Load Balancing health check before
* they signal success, add a health-check verification by using the
* cfn-init helper script. For an example, see the verify_instance_health
* command in the Auto Scaling rolling updates sample template.
*
* @default true if you specified the minSuccessfulInstancesPercent property, false otherwise
*/
waitOnResourceSignals?: boolean;

/**
* Specifies the Auto Scaling processes to suspend during a stack update.
*
* Suspending processes prevents Auto Scaling from interfering with a stack
* update.
*
* @default HealthCheck, ReplaceUnhealthy, AZRebalance, AlarmNotification, ScheduledActions.
*/
suspendProcesses?: ScalingProcess[];
}

export enum ScalingProcess {
Launch = 'Launch',
Terminate = 'Terminate',
HealthCheck = 'HealthCheck',
ReplaceUnhealthy = 'ReplaceUnhealthy',
AZRebalance = 'AZRebalance',
AlarmNotification = 'AlarmNotification',
ScheduledActions = 'ScheduledActions',
AddToLoadBalancer = 'AddToLoadBalancer'
}

/**
* Render the rolling update configuration into the appropriate object
*/
function renderRollingUpdateConfig(config: RollingUpdateConfiguration = {}): cdk.AutoScalingRollingUpdate {
const waitOnResourceSignals = config.minSuccessfulInstancesPercent !== undefined ? true : false;
const pauseTimeSec = config.pauseTimeSec !== undefined ? config.pauseTimeSec : (waitOnResourceSignals ? 300 : 0);

return {
maxBatchSize: config.maxBatchSize,
minInstancesInService: config.minInstancesInService,
minSuccessfulInstancesPercent: validatePercentage(config.minSuccessfulInstancesPercent),
waitOnResourceSignals,
pauseTime: renderIsoDuration(pauseTimeSec),
suspendProcesses: config.suspendProcesses !== undefined ? config.suspendProcesses :
// Recommended list of processes to suspend from here:
// https://aws.amazon.com/premiumsupport/knowledge-center/auto-scaling-group-rolling-updates/
[ScalingProcess.HealthCheck, ScalingProcess.ReplaceUnhealthy, ScalingProcess.AZRebalance,
ScalingProcess.AlarmNotification, ScalingProcess.ScheduledActions],
};
}

/**
* Render a number of seconds to a PTnX string.
*/
function renderIsoDuration(seconds: number): string {
const ret: string[] = [];

if (seconds >= 3600) {
ret.push(`${Math.floor(seconds / 3600)}H`);
seconds %= 3600;
}
if (seconds >= 60) {
ret.push(`${Math.floor(seconds / 60)}M`);
seconds %= 60;
}
if (seconds > 0) {
ret.push(`${seconds}S`);
}

return 'PT' + ret.join('');
}

function validatePercentage(x?: number): number | undefined {
if (x === undefined || (0 <= x && x <= 100)) { return x; }
throw new Error(`Expected: a percentage 0..100, got: ${x}`);
}

0 comments on commit 2864b98

Please sign in to comment.