Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(elbv2): support specifying the weighted_random routing algorithm and enabling anomaly mitigation #29972

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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 @@ -298,6 +298,11 @@ export enum TargetGroupLoadBalancingAlgorithmType {
* least_outstanding_requests
*/
LEAST_OUTSTANDING_REQUESTS = 'least_outstanding_requests',

/**
* weighted_random
*/
WEIGHTED_RANDOM = 'weighted_random',
}

/**
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 @@ -1059,6 +1059,103 @@ describe('tests', () => {
});
});

test('weighted_random load balancer algorithm type', () => {
// 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)],
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
});

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

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 @@ -1733,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 @@ -241,6 +241,95 @@ describe('tests', () => {
});
});

test('weighted_random load balancer algorithm type', () => {
// 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', {
loadBalancingAlgorithmType: elbv2.TargetGroupLoadBalancingAlgorithmType.WEIGHTED_RANDOM,
vpc,
});

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

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