Skip to content

Commit

Permalink
feat(elasticloadbalancingv2): Add support for application cookies (#1…
Browse files Browse the repository at this point in the history
…3142)

https://aws.amazon.com/about-aws/whats-new/2021/02/application-load-balancer-supports-application-cookie-stickiness/

Maybe I shouldn't have deprecated `stickinessCookieDuration` ... which is essentially a rename to `loadBalancerStickinessCookieDuration`.... but that can be easily reverted based on the feedback I recieve. 

As always... feedback is much appreciated. Thanks! 👍 

edit:

lb_cookie 

<img width="1377" alt="Screen Shot 2021-02-18 at 5 14 31 PM" src="https://user-images.githubusercontent.com/31543/108447101-858a0d80-721c-11eb-83e7-d017bb3f69e5.png">

app_cookie
<img width="1397" alt="Screen Shot 2021-02-18 at 5 14 16 PM" src="https://user-images.githubusercontent.com/31543/108447114-8ae75800-721c-11eb-8d74-fa17a2d5ec8c.png">


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
robertd authored Feb 24, 2021
1 parent 4553d4b commit 23385dd
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 14 deletions.
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-elasticloadbalancingv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,33 @@ const group = listener.addTargets('AppFleet', {
group.addTarget(asg2);
```

### Sticky sessions for your Application Load Balancer

By default, an Application Load Balancer routes each request independently to a registered target based on the chosen load-balancing algorithm. However, you can use the sticky session feature (also known as session affinity) to enable the load balancer to bind a user's session to a specific target. This ensures that all requests from the user during the session are sent to the same target. This feature is useful for servers that maintain state information in order to provide a continuous experience to clients. To use sticky sessions, the client must support cookies.

Application Load Balancers support both duration-based cookies (`lb_cookie`) and application-based cookies (`app_cookie`). The key to managing sticky sessions is determining how long your load balancer should consistently route the user's request to the same target. Sticky sessions are enabled at the target group level. You can use a combination of duration-based stickiness, application-based stickiness, and no stickiness across all of your target groups.

```ts
// Target group with duration-based stickiness with load-balancer generated cookie
const tg1 = new elbv2.ApplicationTargetGroup(stack, 'TG1', {
targetType: elbv2.TargetType.INSTANCE,
port: 80,
stickinessCookieDuration: cdk.Duration.minutes(5),
vpc,
});

// Target group with application-based stickiness
const tg2 = new elbv2.ApplicationTargetGroup(stack, 'TG2', {
targetType: elbv2.TargetType.INSTANCE,
port: 80,
stickinessCookieDuration: cdk.Duration.minutes(5),
stickinessCookieName: 'MyDeliciousCookie',
vpc,
});
```

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

## Using Lambda Targets

To use a Lambda Function as a target, use the integration class in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis
protocol: props.protocol,
slowStart: props.slowStart,
stickinessCookieDuration: props.stickinessCookieDuration,
stickinessCookieName: props.stickinessCookieName,
targetGroupName: props.targetGroupName,
targets: props.targets,
vpc: this.loadBalancer.vpc,
Expand Down Expand Up @@ -813,6 +814,20 @@ export interface AddApplicationTargetsProps extends AddRuleProps {
*/
readonly stickinessCookieDuration?: Duration;

/**
* The name of an application-based stickiness cookie.
*
* Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP,
* and AWSALBTG; they're reserved for use by the load balancer.
*
* Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter.
* If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted.
*
* @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined.
* @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html
*/
readonly stickinessCookieName?: string;

/**
* The targets to add to this target group.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import { Annotations, Duration } from '@aws-cdk/core';
import { Annotations, Duration, Token } from '@aws-cdk/core';
import { IConstruct, Construct } from 'constructs';
import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated';
import {
Expand Down Expand Up @@ -57,6 +57,20 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps {
*/
readonly stickinessCookieDuration?: Duration;

/**
* The name of an application-based stickiness cookie.
*
* Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP,
* and AWSALBTG; they're reserved for use by the load balancer.
*
* Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter.
* If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted.
*
* @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined.
* @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html
*/
readonly stickinessCookieName?: string;

/**
* The targets to add to this target group.
*
Expand Down Expand Up @@ -111,8 +125,8 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat
if (props.slowStart !== undefined) {
this.setAttribute('slow_start.duration_seconds', props.slowStart.toSeconds().toString());
}
if (props.stickinessCookieDuration !== undefined) {
this.enableCookieStickiness(props.stickinessCookieDuration);
if (props.stickinessCookieDuration) {
this.enableCookieStickiness(props.stickinessCookieDuration, props.stickinessCookieName);
}
this.addTarget(...(props.targets || []));
}
Expand All @@ -129,12 +143,31 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat
}

/**
* Enable sticky routing via a cookie to members of this target group
* Enable sticky routing via a cookie to members of this target group.
*
* Note: If the `cookieName` parameter is set, application-based stickiness will be applied,
* otherwise it defaults to duration-based stickiness attributes (`lb_cookie`).
*
* @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html
*/
public enableCookieStickiness(duration: Duration) {
public enableCookieStickiness(duration: Duration, cookieName?: string) {
if (cookieName !== undefined) {
if (!Token.isUnresolved(cookieName) && (cookieName.startsWith('AWSALB') || cookieName.startsWith('AWSALBAPP') || cookieName.startsWith('AWSALBTG'))) {
throw new Error('App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer.');
}
if (cookieName === '') {
throw new Error('App cookie name cannot be an empty string.');
}
}
this.setAttribute('stickiness.enabled', 'true');
this.setAttribute('stickiness.type', 'lb_cookie');
this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString());
if (cookieName) {
this.setAttribute('stickiness.type', 'app_cookie');
this.setAttribute('stickiness.app_cookie.cookie_name', cookieName);
this.setAttribute('stickiness.app_cookie.duration_seconds', duration.toSeconds().toString());
} else {
this.setAttribute('stickiness.type', 'lb_cookie');
this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('tests', () => {
});
});

test('Listener default to open - IPv4 and IPv6 (dualstack)', () => {
test('Listener default to open - IPv4 and IPv6 (dual stack)', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Stack');
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('tests', () => {
});
});

test('Enable stickiness for targets', () => {
test('Enable alb stickiness for targets', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Stack');
Expand Down Expand Up @@ -349,6 +349,43 @@ describe('tests', () => {
});
});

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

// WHEN
const group = listener.addTargets('Group', {
port: 80,
targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)],
});
group.enableCookieStickiness(cdk.Duration.hours(1), 'MyDeliciousCookie');

// THEN
expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', {
TargetGroupAttributes: [
{
Key: 'stickiness.enabled',
Value: 'true',
},
{
Key: 'stickiness.type',
Value: 'app_cookie',
},
{
Key: 'stickiness.app_cookie.cookie_name',
Value: 'MyDeliciousCookie',
},
{
Key: 'stickiness.app_cookie.duration_seconds',
Value: '3600',
},
],
});
});

test('Enable health check for targets', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -823,7 +860,7 @@ describe('tests', () => {
});
});

test('Throws when specifying both target groups and fixed reponse', () => {
test('Throws when specifying both target groups and fixed response', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
Expand Down Expand Up @@ -868,7 +905,7 @@ describe('tests', () => {
})).toThrowError('Priority must have value greater than or equal to 1');
});

test('Throws when specifying both target groups and redirect reponse', () => {
test('Throws when specifying both target groups and redirect response', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
Expand Down Expand Up @@ -970,7 +1007,7 @@ describe('tests', () => {
});
});

test('Can add additional certificates via addCertficateArns to application listener', () => {
test('Can add additional certificates via addCertificateArns to application listener', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Stack');
Expand Down Expand Up @@ -1050,7 +1087,7 @@ describe('tests', () => {
})).toThrowError('Both `pathPatterns` and `pathPattern` are specified, specify only one');
});

test('Add additonal condition to listener rule', () => {
test('Add additional condition to listener rule', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Stack');
Expand Down Expand Up @@ -1244,7 +1281,7 @@ describe('tests', () => {
});
});

test('Can exist together legacy style conditions and modan style conditions', () => {
test('Can exist together legacy style conditions and modern style conditions', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Stack');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,106 @@ describe('tests', () => {
UnhealthyThresholdCount: 27,
});
});

test('Load balancer duration cookie stickiness', () => {
// 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', {
stickinessCookieDuration: cdk.Duration.minutes(5),
vpc,
});

// THEN
expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', {
TargetGroupAttributes: [
{
Key: 'stickiness.enabled',
Value: 'true',
},
{
Key: 'stickiness.type',
Value: 'lb_cookie',
},
{
Key: 'stickiness.lb_cookie.duration_seconds',
Value: '300',
},
],
});
});

test('Load balancer app cookie stickiness', () => {
// 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', {
stickinessCookieDuration: cdk.Duration.minutes(5),
stickinessCookieName: 'MyDeliciousCookie',
vpc,
});

// THEN
expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', {
TargetGroupAttributes: [
{
Key: 'stickiness.enabled',
Value: 'true',
},
{
Key: 'stickiness.type',
Value: 'app_cookie',
},
{
Key: 'stickiness.app_cookie.cookie_name',
Value: 'MyDeliciousCookie',
},
{
Key: 'stickiness.app_cookie.duration_seconds',
Value: '300',
},
],
});
});

test('Bad stickiness cookie names', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const vpc = new ec2.Vpc(stack, 'VPC', {});
const errMessage = 'App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer';

// THEN
['AWSALBCookieName', 'AWSALBstickinessCookieName', 'AWSALBTGCookieName'].forEach((badCookieName, i) => {
expect(() => {
new elbv2.ApplicationTargetGroup(stack, `TargetGroup${i}`, {
stickinessCookieDuration: cdk.Duration.minutes(5),
stickinessCookieName: badCookieName,
vpc,
});
}).toThrow(errMessage);
});
});

test('Empty stickiness cookie name', () => {
// 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', {
stickinessCookieDuration: cdk.Duration.minutes(5),
stickinessCookieName: '',
vpc,
});
}).toThrow(/App cookie name cannot be an empty string./);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,20 @@
"Properties": {
"Port": 80,
"Protocol": "HTTP",
"TargetGroupAttributes": [
{
"Key": "stickiness.enabled",
"Value": "true"
},
{
"Key": "stickiness.type",
"Value": "lb_cookie"
},
{
"Key": "stickiness.lb_cookie.duration_seconds",
"Value": "300"
}
],
"Targets": [
{
"Id": "10.0.128.4"
Expand All @@ -454,6 +468,24 @@
"Properties": {
"Port": 80,
"Protocol": "HTTP",
"TargetGroupAttributes": [
{
"Key": "stickiness.enabled",
"Value": "true"
},
{
"Key": "stickiness.type",
"Value": "app_cookie"
},
{
"Key": "stickiness.app_cookie.cookie_name",
"Value": "MyDeliciousCookie"
},
{
"Key": "stickiness.app_cookie.duration_seconds",
"Value": "300"
}
],
"Targets": [
{
"Id": "10.0.128.5"
Expand Down
Loading

0 comments on commit 23385dd

Please sign in to comment.