packages/aws-cdk-lib/aws-autoscaling/lib/step-scaling-policy.ts (136 lines of code) (raw):

import { Construct } from 'constructs'; import { IAutoScalingGroup } from './auto-scaling-group'; import { AdjustmentType, MetricAggregationType, StepScalingAction } from './step-scaling-action'; import { findAlarmThresholds, normalizeIntervals } from '../../aws-autoscaling-common'; import * as cloudwatch from '../../aws-cloudwatch'; import { Duration, Token, ValidationError } from '../../core'; export interface BasicStepScalingPolicyProps { /** * Metric to scale on. */ readonly metric: cloudwatch.IMetric; /** * The intervals for scaling. * * Maps a range of metric values to a particular scaling behavior. * * Must be between 2 and 40 steps. */ readonly scalingSteps: ScalingInterval[]; /** * How the adjustment numbers inside 'intervals' are interpreted. * * @default ChangeInCapacity */ readonly adjustmentType?: AdjustmentType; /** * Grace period after scaling activity. * * @default Default cooldown period on your AutoScalingGroup */ readonly cooldown?: Duration; /** * Estimated time until a newly launched instance can send metrics to CloudWatch. * * @default Same as the cooldown */ readonly estimatedInstanceWarmup?: Duration; /** * Minimum absolute number to adjust capacity with as result of percentage scaling. * * Only when using AdjustmentType = PercentChangeInCapacity, this number controls * the minimum absolute effect size. * * @default No minimum scaling effect */ readonly minAdjustmentMagnitude?: number; /** * How many evaluation periods of the metric to wait before triggering a scaling action * * Raising this value can be used to smooth out the metric, at the expense * of slower response times. * * If `datapointsToAlarm` is not set, then all data points in the evaluation period * must meet the criteria to trigger a scaling action. * * @default 1 */ readonly evaluationPeriods?: number; /** * The number of data points out of the evaluation periods that must be breaching to * trigger a scaling action * * Creates an "M out of N" alarm, where this property is the M and the value set for * `evaluationPeriods` is the N value. * * Only has meaning if `evaluationPeriods != 1`. Must be less than or equal to * `evaluationPeriods`. * * @default - Same as `evaluationPeriods` */ readonly datapointsToAlarm?: number; /** * Aggregation to apply to all data points over the evaluation periods * * Only has meaning if `evaluationPeriods != 1`. * * @default - The statistic from the metric if applicable (MIN, MAX, AVERAGE), otherwise AVERAGE. */ readonly metricAggregationType?: MetricAggregationType; } export interface StepScalingPolicyProps extends BasicStepScalingPolicyProps { /** * The auto scaling group */ readonly autoScalingGroup: IAutoScalingGroup; } /** * Define a acaling strategy which scales depending on absolute values of some metric. * * You can specify the scaling behavior for various values of the metric. * * Implemented using one or more CloudWatch alarms and Step Scaling Policies. */ export class StepScalingPolicy extends Construct { public readonly lowerAlarm?: cloudwatch.Alarm; public readonly lowerAction?: StepScalingAction; public readonly upperAlarm?: cloudwatch.Alarm; public readonly upperAction?: StepScalingAction; constructor(scope: Construct, id: string, props: StepScalingPolicyProps) { super(scope, id); if (props.scalingSteps.length < 2) { throw new ValidationError('You must supply at least 2 intervals for autoscaling', this); } if (props.scalingSteps.length > 40) { throw new ValidationError(`'scalingSteps' can have at most 40 steps, got ${props.scalingSteps.length}`, this); } if (props.evaluationPeriods !== undefined && !Token.isUnresolved(props.evaluationPeriods) && props.evaluationPeriods < 1) { throw new ValidationError(`evaluationPeriods cannot be less than 1, got: ${props.evaluationPeriods}`, this); } if (props.datapointsToAlarm !== undefined) { if (props.evaluationPeriods === undefined) { throw new ValidationError('evaluationPeriods must be set if datapointsToAlarm is set', this); } if (!Token.isUnresolved(props.datapointsToAlarm) && props.datapointsToAlarm < 1) { throw new ValidationError(`datapointsToAlarm cannot be less than 1, got: ${props.datapointsToAlarm}`, this); } if (!Token.isUnresolved(props.datapointsToAlarm) && !Token.isUnresolved(props.evaluationPeriods) && props.evaluationPeriods < props.datapointsToAlarm ) { throw new ValidationError(`datapointsToAlarm must be less than or equal to evaluationPeriods, got datapointsToAlarm: ${props.datapointsToAlarm}, evaluationPeriods: ${props.evaluationPeriods}`, this); } } const adjustmentType = props.adjustmentType || AdjustmentType.CHANGE_IN_CAPACITY; const changesAreAbsolute = adjustmentType === AdjustmentType.EXACT_CAPACITY; const intervals = normalizeIntervals(props.scalingSteps, changesAreAbsolute); const alarms = findAlarmThresholds(intervals); if (alarms.lowerAlarmIntervalIndex !== undefined) { const threshold = intervals[alarms.lowerAlarmIntervalIndex].upper; this.lowerAction = new StepScalingAction(this, 'LowerPolicy', { adjustmentType, cooldown: props.cooldown, estimatedInstanceWarmup: props.estimatedInstanceWarmup, metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, autoScalingGroup: props.autoScalingGroup, }); for (let i = alarms.lowerAlarmIntervalIndex; i >= 0; i--) { this.lowerAction.addAdjustment({ adjustment: intervals[i].change!, lowerBound: i !== 0 ? intervals[i].lower - threshold : undefined, // Extend last interval to -infinity upperBound: intervals[i].upper - threshold, }); } this.lowerAlarm = new cloudwatch.Alarm(this, 'LowerAlarm', { // Recommended by AutoScaling metric: props.metric, alarmDescription: 'Lower threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, evaluationPeriods: props.evaluationPeriods ?? 1, datapointsToAlarm: props.datapointsToAlarm, threshold, }); this.lowerAlarm.addAlarmAction(new StepScalingAlarmAction(this.lowerAction)); } if (alarms.upperAlarmIntervalIndex !== undefined) { const threshold = intervals[alarms.upperAlarmIntervalIndex].lower; this.upperAction = new StepScalingAction(this, 'UpperPolicy', { adjustmentType, cooldown: props.cooldown, estimatedInstanceWarmup: props.estimatedInstanceWarmup, metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, autoScalingGroup: props.autoScalingGroup, }); for (let i = alarms.upperAlarmIntervalIndex; i < intervals.length; i++) { this.upperAction.addAdjustment({ adjustment: intervals[i].change!, lowerBound: intervals[i].lower - threshold, upperBound: i !== intervals.length - 1 ? intervals[i].upper - threshold : undefined, // Extend last interval to +infinity }); } this.upperAlarm = new cloudwatch.Alarm(this, 'UpperAlarm', { // Recommended by AutoScaling metric: props.metric, alarmDescription: 'Upper threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, evaluationPeriods: props.evaluationPeriods ?? 1, datapointsToAlarm: props.datapointsToAlarm, threshold, }); this.upperAlarm.addAlarmAction(new StepScalingAlarmAction(this.upperAction)); } } } function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregationType | undefined { const statistic = metric.toMetricConfig().metricStat?.statistic; if (statistic === undefined) { return undefined; } // Math expression, don't know aggregation, leave default switch (statistic) { case 'Average': return MetricAggregationType.AVERAGE; case 'Minimum': return MetricAggregationType.MINIMUM; case 'Maximum': return MetricAggregationType.MAXIMUM; default: return MetricAggregationType.AVERAGE; } } /** * 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 */ readonly 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 */ readonly 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. */ readonly change: number; } /** * Use a StepScalingAction as an Alarm Action * * This class is here and not in aws-cloudwatch-actions because this library * needs to use the class, and otherwise we'd have a circular dependency: * * aws-autoscaling -> aws-cloudwatch-actions (for using the Action) * aws-cloudwatch-actions -> aws-autoscaling (for the definition of IStepScalingAction) */ class StepScalingAlarmAction implements cloudwatch.IAlarmAction { constructor(private readonly stepScalingAction: StepScalingAction) { } public bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.stepScalingAction.scalingPolicyArn }; } }