Skip to content

Commit

Permalink
feat(aws-autoscaling): add instance AutoScaling
Browse files Browse the repository at this point in the history
It's now possible to add autoscaling policies (step scaling, target
tracking, and scheduled) to AutoScalingGroup. It's also possible to add
lifecycle hooks to an AutoScalingGroup, which will publish messages to
SQS queues or SNS topics.

ALSO IN THIS COMMIT

- Fix an issue where an invalid PauseTime is generated on
  AutoScalingGroup.

Fixes #1042, fixes #1113.
  • Loading branch information
Rico Huijbers authored and rix0rrr committed Nov 9, 2018
1 parent 59d0c51 commit 8361833
Show file tree
Hide file tree
Showing 52 changed files with 82,498 additions and 276 deletions.
17 changes: 9 additions & 8 deletions packages/@aws-cdk/aws-applicationautoscaling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ scaling actions might, see below).

There are three ways to scale your capacity:

* **In response to a metric**; for example, you might want to scale out
if the CPU usage across your cluster starts to rise, and scale in
when it drops again.
* **By trying to keep a certain metric around a given value**; you might
want to automatically scale out an in to keep your CPU usage around 50%.
* **In response to a metric** (also known as step scaling); for example, you
might want to scale out if the CPU usage across your cluster starts to rise,
and scale in when it drops again.
* **By trying to keep a certain metric around a given value** (also known as
target tracking scaling); you might want to automatically scale out an in to
keep your CPU usage around 50%.
* **On a schedule**; you might want to organize your scaling around traffic
flows you expect, by scaling out in the morning and scaling in in the
evening.
Expand All @@ -50,7 +51,7 @@ capacity.scaleToTrackMetric(...);
capacity.scaleOnSchedule(...);
```

### AutoScaling in response to a metric
### Step Scaling

This type of scaling scales in and out in deterministics steps that you
configure, in response to metric values. For example, your scaling strategy
Expand Down Expand Up @@ -87,7 +88,7 @@ capacity.scaleOnMetric('ScaleToCPU', {
The AutoScaling construct library will create the required CloudWatch alarms and
AutoScaling policies for you.

### AutoScaling by tracking a metric value
### Target Tracking Scaling

This type of scaling scales in and out in order to keep a metric (typically
representing utilization) around a value you prefer. This type of scaling is
Expand All @@ -108,7 +109,7 @@ readCapacity.scaleOnUtilization({
});
```

### AutoScaling on a schedule
### Scheduled Scaling

This type of scaling is used to change capacities based on time. It works
by changing the `minCapacity` and `maxCapacity` of the attribute, and so
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { findAlarmThresholds, normalizeIntervals } from '@aws-cdk/aws-autoscaling-common';
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import cdk = require('@aws-cdk/cdk');
import { findAlarmThresholds, normalizeIntervals } from './interval-utils';
import { ScalableTarget } from './scalable-target';
import { AdjustmentType, MetricAggregationType, StepScalingAction } from './step-scaling-action';

Expand Down Expand Up @@ -81,7 +81,7 @@ export class StepScalingPolicy extends cdk.Construct {
const intervals = normalizeIntervals(props.scalingSteps, changesAreAbsolute);
const alarms = findAlarmThresholds(intervals);

if (alarms.lowerAlarmIntervalIndex) {
if (alarms.lowerAlarmIntervalIndex !== undefined) {
const threshold = intervals[alarms.lowerAlarmIntervalIndex].upper;

this.lowerAction = new StepScalingAction(this, 'LowerPolicy', {
Expand All @@ -104,14 +104,14 @@ export class StepScalingPolicy extends cdk.Construct {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
alarmDescription: 'Lower threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.LessThanThreshold,
comparisonOperator: cloudwatch.ComparisonOperator.LessThanOrEqualToThreshold,
evaluationPeriods: 1,
threshold,
});
this.lowerAlarm.onAlarm(this.lowerAction);
}

if (alarms.upperAlarmIntervalIndex) {
if (alarms.upperAlarmIntervalIndex !== undefined) {
const threshold = intervals[alarms.upperAlarmIntervalIndex].lower;

this.upperAction = new StepScalingAction(this, 'UpperPolicy', {
Expand All @@ -134,7 +134,7 @@ export class StepScalingPolicy extends cdk.Construct {
// Recommended by AutoScaling
metric: props.metric.with({ periodSec: 60 }),
alarmDescription: 'Upper threshold scaling alarm',
comparisonOperator: cloudwatch.ComparisonOperator.GreaterThanThreshold,
comparisonOperator: cloudwatch.ComparisonOperator.GreaterThanOrEqualToThreshold,
evaluationPeriods: 1,
threshold,
});
Expand All @@ -143,9 +143,40 @@ export class StepScalingPolicy extends cdk.Construct {
}
}

/**
* A range of metric values in which to apply a certain scaling operation
*/
export interface ScalingInterval {
/**
* The lower bound of the interval.
*
* The scaling adjustment will be applied if the metric is higher than this value.
*
* @default Threshold automatically derived from neighbouring intervals
*/
lower?: number;

/**
* The upper bound of the interval.
*
* The scaling adjustment will be applied if the metric is lower than this value.
*
* @default Threshold automatically derived from neighbouring intervals
*/
upper?: number;

/**
* The capacity adjustment to apply in this interval
*
* The number is interpreted differently based on AdjustmentType:
*
* - ChangeInCapacity: add the adjustment to the current capacity.
* The number can be positive or negative.
* - PercentChangeInCapacity: add or remove the given percentage of the current
* capacity to itself. The number can be in the range [-100..100].
* - ExactCapacity: set the capacity to this number. The number must
* be positive.
*/
change: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import appscaling = require('../lib');

export = {
'test utc cron, hour only'(test: Test) {
test.equals(appscaling.Cron.dailyUtc(18), 'cron(0 18 * * ?)');
test.equals(appscaling.Cron.dailyUtc(18), 'cron(0 18 * * *)');
test.done();
},

'test utc cron, hour and minute'(test: Test) {
test.equals(appscaling.Cron.dailyUtc(18, 24), 'cron(24 18 * * ?)');
test.equals(appscaling.Cron.dailyUtc(18, 24), 'cron(24 18 * * *)');
test.done();
}
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { arbitrary_input_intervals } from '@aws-cdk/aws-autoscaling-common';
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import cdk = require('@aws-cdk/cdk');
import fc = require('fast-check');
import { Test } from 'nodeunit';
import appscaling = require('../lib');
import { arbitrary_input_intervals, createScalableTarget } from './util';
import { createScalableTarget } from './util';

export = {
'alarm thresholds are valid numbers'(test: Test) {
Expand Down
112 changes: 0 additions & 112 deletions packages/@aws-cdk/aws-applicationautoscaling/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,6 @@
import cdk = require('@aws-cdk/cdk');
import fc = require('fast-check');
import appscaling = require('../lib');
import { ServiceNamespace } from '../lib';
import { normalizeIntervals } from '../lib/interval-utils';

/**
* Arbitrary (valid) array of intervals
*
* There are many invalid combinations of interval arrays, so we have
* to be very specific about generating arrays that are valid. We do this
* by taking a full, valid interval schedule and progressively stripping parts
* away from it.
*
* Some of the changes may change its meaning, but we take care to never leave
* a schedule with insufficient information so that the parser will error out.
*/
export class ArbitraryIntervals extends fc.Arbitrary<appscaling.ScalingInterval[]> {
public generate(mrng: fc.Random): fc.Shrinkable<appscaling.ScalingInterval[]> {
const ret = new Array<appscaling.ScalingInterval>();

const absolute = mrng.nextBoolean();

// Ascending or descending scaling
const factor = (mrng.nextBoolean() ? 1 : -1) * (absolute ? 10 : 1);
const bias = absolute ? 50 : 0;

// Begin with a full schedule
ret.push({ lower: 0, upper: 10, change: -2 * factor + bias });
ret.push({ lower: 10, upper: 20, change: -1 * factor + bias });
ret.push({ lower: 20, upper: 60, change: 0 + bias });
ret.push({ lower: 60, upper: 80, change: 0 + bias });
ret.push({ lower: 80, upper: 90, change: 1 * factor + bias });
ret.push({ lower: 90, upper: Infinity, change: 2 * factor + bias});

// Take away parts from this. First we see if we do something to the 0-change alarms.
// The actions can be: remove it OR turn it into a regular change value.
const noChanges = ret.filter(x => x.change === bias);

if (!absolute) {
if (mrng.nextBoolean()) {
if (mrng.nextBoolean()) {
ret.splice(ret.indexOf(noChanges[0]), 1);
} else {
noChanges[0].change = -1 * factor + bias;
}
}
if (mrng.nextBoolean()) {
if (mrng.nextBoolean()) {
ret.splice(ret.indexOf(noChanges[1]), 1);
} else {
noChanges[1].change = 1 * factor + bias;
}
}
} else {
// In absolute mode both have to get the same treatment at the same time
// otherwise we'll end up with a timeline with two gaps
if (mrng.nextBoolean()) {
ret.splice(ret.indexOf(noChanges[0]), 1);
ret.splice(ret.indexOf(noChanges[1]), 1);
} else {
noChanges[0].change = -1 * factor + bias;
noChanges[1].change = 1 * factor + bias;
}
}

// We might also take away either the bottom or the upper half
if (mrng.nextInt(0, 2) === 0) {
const signToStrip = mrng.nextBoolean() ? -1 : 1;
let ix = ret.findIndex(x => Math.sign(x.change - bias) === signToStrip);
while (ix >= 0) {
ret.splice(ix, 1);
ix = ret.findIndex(x => Math.sign(x.change - bias) === signToStrip);
}
}

// Then we're going to arbitrarily get rid of bounds in the most naive way possible
const iterations = mrng.nextInt(0, 10);
for (let iter = 0; iter < iterations; iter++) {
const i = mrng.nextInt(0, ret.length - 1);
if (mrng.nextBoolean()) {
// scrap lower bound
// okay if current interval has an upper bound AND the preceding interval has an upper bound
if (ret[i].upper !== undefined && (i === 0 || ret[i - 1].upper !== undefined)) {
ret[i].lower = undefined;
}
} else {
// scrap upper bound
// okay if current interval has a lower bound AND the succeeding interval has a lower bound
if (ret[i].lower !== undefined && (i === ret.length - 1 || ret[i + 1].lower !== undefined)) {
ret[i].upper = undefined;
}
}
}

// Hide a property on the array
(ret as any).absolute = absolute;

// Shrinkable that doesn't actually shrink
return new fc.Shrinkable(ret);
}
}

export function arbitrary_input_intervals() {
return new ArbitraryIntervals();
}

/**
* Normalized interval array
*/
export function arbitrary_complete_intervals() {
return new ArbitraryIntervals().map(x => {
return normalizeIntervals(x, (x as any).absolute);
});
}

export function createScalableTarget(parent: cdk.Construct) {
return new appscaling.ScalableTarget(parent, 'Target', {
Expand Down
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-autoscaling-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
lambda/bundle.zip

*.js
tsconfig.json
tslint.json
*.js.map
*.d.ts
*.generated.ts
dist
lib/generated/resources.ts
.jsii
lib/*.zip

.LAST_BUILD
.nyc_output
coverage
.nycrc
.LAST_PACKAGE

*.snk
19 changes: 19 additions & 0 deletions packages/@aws-cdk/aws-autoscaling-api/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Don't include original .ts files when doing `npm pack`
*.ts
!*.d.ts
coverage
.nyc_output
*.tgz

dist
.LAST_PACKAGE
.LAST_BUILD
!*.js

# Include .jsii
!.jsii

lambda/src
lambda/test
lambda/*.sh
*.snk
Loading

0 comments on commit 8361833

Please sign in to comment.