Skip to content

Commit

Permalink
feat(aws-elbv2): add support for enabling anomaly mitigation for targ…
Browse files Browse the repository at this point in the history
…et groups
  • Loading branch information
pag-tractive authored and gillesbergerp committed Apr 26, 2024
1 parent 66de1e1 commit ef79da7
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 35 deletions.
@@ -1,47 +1,21 @@
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { App, CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';

class TestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 1, restrictDefaultSecurityGroup: false });

const task = new ecs.FargateTaskDefinition(this, 'Task', { cpu: 256, memoryLimitMiB: 512 });
task.addContainer('nginx', {
image: ecs.ContainerImage.fromRegistry('public.ecr.aws/nginx/nginx:latest'),
portMappings: [{ containerPort: 80 }],
});
const svc = new patterns.ApplicationLoadBalancedFargateService(this, 'Service', {
vpc,
taskDefinition: task,
publicLoadBalancer: false,
});

const nlb = new elbv2.NetworkLoadBalancer(this, 'Nlb', {
vpc,
crossZoneEnabled: true,
internetFacing: true,
});
const listener = nlb.addListener('listener', {
new elbv2.ApplicationTargetGroup(this, 'TG', {
targetType: elbv2.TargetType.INSTANCE,
loadBalancingAlgorithmAnomalyDetection: true,
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
port: 80,
vpc,
});

const target = listener.addTargets('Targets', {
targets: [new targets.AlbTarget(svc.loadBalancer, 80)],
port: 80,
healthCheck: {
protocol: elbv2.Protocol.HTTP,
},
});
target.node.addDependency(svc.listener);

new CfnOutput(this, 'NlbEndpoint', { value: `http://${nlb.loadBalancerDnsName}` });
}
}

Expand Down
24 changes: 24 additions & 0 deletions packages/aws-cdk-lib/aws-elasticloadbalancingv2/README.md
Expand Up @@ -438,6 +438,30 @@ const tg = new elbv2.ApplicationTargetGroup(this, 'TG', {

For more information see: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html#application-based-stickiness

### Anomaly mitigation for your Application Load Balancer

By default, requests are routed using the Round Robin algorithm.
An alternative is Automatic Target Weights (ATW) which also allows for enabling anomaly mitigation.
When anomalous targets are identified, ATW can automatically attempt to stabilize them by reducing the amount of traffic they're routed, known as anomaly mitigation.
ATW continuously optimizes traffic distribution to maximize per-target success rates while minimizing target group failure rates.

```ts
declare const vpc: ec2.Vpc;

// Target group with slow start mode enabled
const tg = new elbv2.ApplicationTargetGroup(this, 'TG', {
targetType: elbv2.TargetType.INSTANCE,
loadBalancingAlgorithmAnomalyDetection: true,
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
port: 80,
vpc,
});
```

For more information see:
* https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#automatic-target-weights
* https://aws.amazon.com/blogs/networking-and-content-delivery/improving-availability-with-application-load-balancer-automatic-target-weights/

### Setting the target group protocol version

By default, Application Load Balancers send requests to targets using HTTP/1.1. You can use the [protocol version](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-protocol-version) to send requests to targets using HTTP/2 or gRPC.
Expand Down
Expand Up @@ -932,6 +932,13 @@ export interface AddApplicationTargetsProps extends AddRuleProps {
*/
readonly loadBalancingAlgorithmType?: TargetGroupLoadBalancingAlgorithmType;

/**
* Indicates whether anomaly mitigation is enabled.
*
* @default false
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-targetgroupattribute.html
*/
readonly loadBalancingAlgorithmAnomalyDetection?: boolean;
}

/**
Expand Down
Expand Up @@ -85,6 +85,14 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps {
*/
readonly loadBalancingAlgorithmType?: TargetGroupLoadBalancingAlgorithmType;

/**
* Indicates whether anomaly mitigation is enabled.
*
* @default false
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-targetgroupattribute.html
*/
readonly loadBalancingAlgorithmAnomalyDetection?: boolean;

/**
* The targets to add to this target group.
*
Expand Down Expand Up @@ -346,6 +354,19 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat
if (props.loadBalancingAlgorithmType) {
this.setAttribute('load_balancing.algorithm.type', props.loadBalancingAlgorithmType);
}

if (props.loadBalancingAlgorithmAnomalyDetection) {
if (props.loadBalancingAlgorithmType != TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM) {
throw new Error('Anomaly mitigation is only supported for the weighted_random load balancing algorithm.');
}

if (!!props.slowStart) {
throw new Error('Anomaly mitigation is not compatible with slow start.');
}

this.setAttribute('load_balancing.algorithm.anomaly_mitigation', 'on');
}

this.addTarget(...(props.targets || []));
}
}
Expand Down
Expand Up @@ -665,9 +665,9 @@ describe('tests', () => {
['',
[{ 'Fn::Select': [1, { 'Fn::Split': ['/', loadBalancerArn] }] },
'/',
{ 'Fn::Select': [2, { 'Fn::Split': ['/', loadBalancerArn] }] },
{ 'Fn::Select': [2, { 'Fn::Split': ['/', loadBalancerArn] }] },
'/',
{ 'Fn::Select': [3, { 'Fn::Split': ['/', loadBalancerArn] }] }]],
{ 'Fn::Select': [3, { 'Fn::Split': ['/', loadBalancerArn] }] }]],
},
});
}
Expand Down Expand Up @@ -1088,6 +1088,74 @@ describe('tests', () => {
});
});

test('Enable anomaly mitigation', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
const listener = lb.addListener('Listener', { port: 80 });

// WHEN
listener.addTargets('Group', {
port: 80,
targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)],
loadBalancingAlgorithmAnomalyDetection: true,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', {
TargetGroupAttributes: [
{
Key: 'stickiness.enabled',
Value: 'false',
},
{
Key: 'load_balancing.algorithm.anomaly_detection',
Value: 'on',
},
],
});
});

test('Throw when trying to enable anomaly mitigation with an unsupported load balancing algorithm', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
const listener = lb.addListener('Listener', { port: 80 });

[elbv2.TargetGroupLoadBalancingAlgorithmType.LEAST_OUTSTANDING_REQUESTS, elbv2.TargetGroupLoadBalancingAlgorithmType.ROUND_ROBIN].forEach((loadBalancingAlgorithmType) => {
// THEN
expect(() => {
listener.addTargets('Group', {
port: 80,
targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)],
loadBalancingAlgorithmType,
loadBalancingAlgorithmAnomalyDetection: true,
});
}).toThrow(/Anomaly mitigation is only supported for the weighted_random load balancing algorithm./);
});
});

test('Throw when trying to enable anomaly mitigation with a slow start period', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
const listener = lb.addListener('Listener', { port: 80 });

// THEN
expect(() => {
listener.addTargets('Group', {
port: 80,
targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)],
loadBalancingAlgorithmAnomalyDetection: true,
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
slowStart: cdk.Duration.seconds(90),
});
}).toThrow(/Anomaly mitigation is not compatible with slow start./);
});

describeDeprecated('Throws with bad fixed responses', () => {

test('status code', () => {
Expand Down Expand Up @@ -1762,7 +1830,7 @@ describe('tests', () => {
});

// THEN
const applicationListenerRule = listener.node.children.find((v)=> v.hasOwnProperty('conditions'));
const applicationListenerRule = listener.node.children.find((v) => v.hasOwnProperty('conditions'));
expect(applicationListenerRule).toBeDefined();
expect(applicationListenerRule!.node.id).toBe(expectedLogicalId);
});
Expand Down
Expand Up @@ -268,6 +268,68 @@ describe('tests', () => {
});
});

test('Enable anomaly mitigation', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const vpc = new ec2.Vpc(stack, 'VPC', {});

// WHEN
new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', {
loadBalancingAlgorithmAnomalyDetection: true,
vpc,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', {
TargetGroupAttributes: [
{
Key: 'stickiness.enabled',
Value: 'false',
},
{
Key: 'load_balancing.algorithm.anomaly_mitigation',
Value: 'on',
},
],
});
});

test('Throw when trying to enable anomaly mitigation with an unsupported load balancing algorithm', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const vpc = new ec2.Vpc(stack, 'VPC', {});

[elbv2.TargetGroupLoadBalancingAlgorithmType.LEAST_OUTSTANDING_REQUESTS, elbv2.TargetGroupLoadBalancingAlgorithmType.ROUND_ROBIN].forEach((loadBalancingAlgorithmType) => {
// THEN
expect(() => {
new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', {
loadBalancingAlgorithmAnomalyDetection: true,
loadBalancingAlgorithmType,
vpc,
});
}).toThrow(/Anomaly mitigation is only supported for the weighted_random load balancing algorithm./);
});
});

test('Throw when trying to enable anomaly mitigation with a slow start period', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const vpc = new ec2.Vpc(stack, 'VPC', {});

// THEN
expect(() => {
new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', {
loadBalancingAlgorithmAnomalyDetection: true,
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
slowStart: cdk.Duration.seconds(90),
vpc,
});
}).toThrow(/Anomaly mitigation is not compatible with slow start./);
});

test('Can set a protocol version', () => {
// GIVEN
const app = new cdk.App();
Expand Down

0 comments on commit ef79da7

Please sign in to comment.