Skip to content

Commit 750bc8b

Browse files
jogoldrix0rrr
authored andcommitted
feat(elbv2): add fixed response support for application load balancers (#2328)
It follows the same pattern as the one used for `addTargetGroups` with the check on `priority`. This allows for the following scenario: an infrastructure stack exporting an application listener with a default fixed response and multiple application stacks registering their services with specific paths/headers on the imported application listener.
1 parent b3792aa commit 750bc8b

File tree

5 files changed

+260
-7
lines changed

5 files changed

+260
-7
lines changed

packages/@aws-cdk/aws-elasticloadbalancingv2/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ listener.addTargets('ApplicationFleet', {
5050
The security groups of the load balancer and the target are automatically
5151
updated to allow the network traffic.
5252

53+
Use the `addFixedResponse()` method to add fixed response rules on the listener:
54+
55+
```ts
56+
listener.addFixedResponse('Fixed', {
57+
pathPattern: '/ok',
58+
contentType: elbv2.ContentType.TEXT_PLAIN,
59+
messageBody: 'OK',
60+
statusCode: '200'
61+
});
62+
```
63+
5364
#### Conditions
5465

5566
It's possible to route traffic to targets based on conditions in the incoming

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ export interface BaseApplicationListenerRuleProps {
1717
readonly priority: number;
1818

1919
/**
20-
* Target groups to forward requests to
20+
* Target groups to forward requests to. Only one of `targetGroups` or
21+
* `fixedResponse` can be specified.
2122
*/
2223
readonly targetGroups?: IApplicationTargetGroup[];
2324

25+
/**
26+
* Fixed response to return. Only one of `fixedResponse` or
27+
* `targetGroups` can be specified.
28+
*/
29+
readonly fixedResponse?: FixedResponse;
30+
2431
/**
2532
* Rule applies if the requested host matches the indicated host
2633
*
@@ -54,6 +61,41 @@ export interface ApplicationListenerRuleProps extends BaseApplicationListenerRul
5461
readonly listener: IApplicationListener;
5562
}
5663

64+
/**
65+
* The content type for a fixed response
66+
*/
67+
export enum ContentType {
68+
TEXT_PLAIN = 'text/plain',
69+
TEXT_CSS = 'text/css',
70+
TEXT_HTML = 'text/html',
71+
APPLICATION_JAVASCRIPT = 'application/javascript',
72+
APPLICATION_JSON = 'application/json'
73+
}
74+
75+
/**
76+
* A fixed response
77+
*/
78+
export interface FixedResponse {
79+
/**
80+
* The HTTP response code (2XX, 4XX or 5XX)
81+
*/
82+
readonly statusCode: string;
83+
84+
/**
85+
* The content type
86+
*
87+
* @default text/plain
88+
*/
89+
readonly contentType?: ContentType;
90+
91+
/**
92+
* The message
93+
*
94+
* @default no message
95+
*/
96+
readonly messageBody?: string;
97+
}
98+
5799
/**
58100
* Define a new listener rule
59101
*/
@@ -75,6 +117,10 @@ export class ApplicationListenerRule extends cdk.Construct {
75117
throw new Error(`At least one of 'hostHeader' or 'pathPattern' is required when defining a load balancing rule.`);
76118
}
77119

120+
if (props.targetGroups && props.fixedResponse) {
121+
throw new Error('Cannot combine `targetGroups` with `fixedResponse`.');
122+
}
123+
78124
this.listener = props.listener;
79125

80126
const resource = new CfnListenerRule(this, 'Resource', {
@@ -93,6 +139,10 @@ export class ApplicationListenerRule extends cdk.Construct {
93139

94140
(props.targetGroups || []).forEach(this.addTargetGroup.bind(this));
95141

142+
if (props.fixedResponse) {
143+
this.addFixedResponse(props.fixedResponse);
144+
}
145+
96146
this.listenerRuleArn = resource.ref;
97147
}
98148

@@ -114,6 +164,18 @@ export class ApplicationListenerRule extends cdk.Construct {
114164
targetGroup.registerListener(this.listener, this);
115165
}
116166

167+
/**
168+
* Add a fixed response
169+
*/
170+
public addFixedResponse(fixedResponse: FixedResponse) {
171+
validateFixedResponse(fixedResponse);
172+
173+
this.actions.push({
174+
fixedResponseConfig: fixedResponse,
175+
type: 'fixed-response'
176+
});
177+
}
178+
117179
/**
118180
* Validate the rule
119181
*/
@@ -137,3 +199,18 @@ export class ApplicationListenerRule extends cdk.Construct {
137199
return ret;
138200
}
139201
}
202+
203+
/**
204+
* Validate the status code and message body of a fixed response
205+
*
206+
* @internal
207+
*/
208+
export function validateFixedResponse(fixedResponse: FixedResponse) {
209+
if (fixedResponse.statusCode && !/^(2|4|5)\d\d$/.test(fixedResponse.statusCode)) {
210+
throw new Error('`statusCode` must be 2XX, 4XX or 5XX.');
211+
}
212+
213+
if (fixedResponse.messageBody && fixedResponse.messageBody.length > 1024) {
214+
throw new Error('`messageBody` cannot have more than 1024 characters.');
215+
}
216+
}

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { HealthCheck } from '../shared/base-target-group';
55
import { ApplicationProtocol, SslPolicy } from '../shared/enums';
66
import { determineProtocolAndPort } from '../shared/util';
77
import { ApplicationListenerCertificate } from './application-listener-certificate';
8-
import { ApplicationListenerRule } from './application-listener-rule';
8+
import { ApplicationListenerRule, FixedResponse, validateFixedResponse } from './application-listener-rule';
99
import { IApplicationLoadBalancer } from './application-load-balancer';
1010
import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group';
1111

@@ -153,9 +153,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis
153153
* At least one TargetGroup must be added without conditions.
154154
*/
155155
public addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void {
156-
if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) {
157-
throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`);
158-
}
156+
checkAddRuleProps(props);
159157

160158
if (props.priority !== undefined) {
161159
// New rule
@@ -215,6 +213,35 @@ export class ApplicationListener extends BaseListener implements IApplicationLis
215213
return group;
216214
}
217215

216+
/**
217+
* Add a fixed response
218+
*/
219+
public addFixedResponse(id: string, props: AddFixedResponseProps) {
220+
checkAddRuleProps(props);
221+
222+
const fixedResponse: FixedResponse = {
223+
statusCode: props.statusCode,
224+
contentType: props.contentType,
225+
messageBody: props.messageBody
226+
};
227+
228+
validateFixedResponse(fixedResponse);
229+
230+
if (props.priority) {
231+
new ApplicationListenerRule(this, id + 'Rule', {
232+
listener: this,
233+
priority: props.priority,
234+
fixedResponse,
235+
...props
236+
});
237+
} else {
238+
this._addDefaultAction({
239+
fixedResponseConfig: fixedResponse,
240+
type: 'fixed-response'
241+
});
242+
}
243+
}
244+
218245
/**
219246
* Register that a connectable that has been added to this load balancer.
220247
*
@@ -539,3 +566,15 @@ export interface AddApplicationTargetsProps extends AddRuleProps {
539566
*/
540567
readonly healthCheck?: HealthCheck;
541568
}
569+
570+
/**
571+
* Properties for adding a fixed response to a listener
572+
*/
573+
export interface AddFixedResponseProps extends AddRuleProps, FixedResponse {
574+
}
575+
576+
function checkAddRuleProps(props: AddRuleProps) {
577+
if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) {
578+
throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`);
579+
}
580+
}

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ITargetGroup } from './base-target-group';
77
*/
88
export abstract class BaseListener extends cdk.Construct {
99
public readonly listenerArn: string;
10-
private readonly defaultActions: any[] = [];
10+
private readonly defaultActions: CfnListener.ActionProperty[] = [];
1111

1212
constructor(scope: cdk.Construct, id: string, additionalProps: any) {
1313
super(scope, id);
@@ -30,12 +30,20 @@ export abstract class BaseListener extends cdk.Construct {
3030
return [];
3131
}
3232

33+
/**
34+
* Add an action to the list of default actions of this listener
35+
* @internal
36+
*/
37+
protected _addDefaultAction(action: CfnListener.ActionProperty) {
38+
this.defaultActions.push(action);
39+
}
40+
3341
/**
3442
* Add a TargetGroup to the list of default actions of this listener
3543
* @internal
3644
*/
3745
protected _addDefaultTargetGroup(targetGroup: ITargetGroup) {
38-
this.defaultActions.push({
46+
this._addDefaultAction({
3947
targetGroupArn: targetGroup.targetGroupArn,
4048
type: 'forward'
4149
});

packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,124 @@ export = {
482482

483483
test.done();
484484
},
485+
486+
'Can add fixed responses'(test: Test) {
487+
// GIVEN
488+
const stack = new cdk.Stack();
489+
const vpc = new ec2.VpcNetwork(stack, 'VPC');
490+
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
491+
vpc
492+
});
493+
const listener = lb.addListener('Listener', {
494+
port: 80
495+
});
496+
497+
// WHEN
498+
listener.addFixedResponse('Default', {
499+
contentType: elbv2.ContentType.TEXT_PLAIN,
500+
messageBody: 'Not Found',
501+
statusCode: '404'
502+
});
503+
listener.addFixedResponse('Hello', {
504+
priority: 10,
505+
pathPattern: '/hello',
506+
statusCode: '503'
507+
});
508+
509+
// THEN
510+
expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
511+
DefaultActions: [
512+
{
513+
FixedResponseConfig: {
514+
ContentType: 'text/plain',
515+
MessageBody: 'Not Found',
516+
StatusCode: '404'
517+
},
518+
Type: 'fixed-response'
519+
}
520+
]
521+
}));
522+
523+
expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', {
524+
Actions: [
525+
{
526+
FixedResponseConfig: {
527+
StatusCode: '503'
528+
},
529+
Type: 'fixed-response'
530+
}
531+
]
532+
}));
533+
534+
test.done();
535+
},
536+
537+
'Throws with bad fixed responses': {
538+
539+
'status code'(test: Test) {
540+
// GIVEN
541+
const stack = new cdk.Stack();
542+
const vpc = new ec2.VpcNetwork(stack, 'VPC');
543+
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
544+
vpc
545+
});
546+
const listener = lb.addListener('Listener', {
547+
port: 80
548+
});
549+
550+
// THEN
551+
test.throws(() => listener.addFixedResponse('Default', {
552+
statusCode: '301'
553+
}), /`statusCode`/);
554+
555+
test.done();
556+
},
557+
558+
'message body'(test: Test) {
559+
// GIVEN
560+
const stack = new cdk.Stack();
561+
const vpc = new ec2.VpcNetwork(stack, 'VPC');
562+
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
563+
vpc
564+
});
565+
const listener = lb.addListener('Listener', {
566+
port: 80
567+
});
568+
569+
// THEN
570+
test.throws(() => listener.addFixedResponse('Default', {
571+
messageBody: 'a'.repeat(1025),
572+
statusCode: '500'
573+
}), /`messageBody`/);
574+
575+
test.done();
576+
}
577+
},
578+
579+
'Throws when specifying both target groups and fixed reponse'(test: Test) {
580+
// GIVEN
581+
const stack = new cdk.Stack();
582+
const vpc = new ec2.VpcNetwork(stack, 'VPC');
583+
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
584+
vpc
585+
});
586+
const listener = lb.addListener('Listener', {
587+
port: 80
588+
});
589+
590+
// THEN
591+
test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', {
592+
listener,
593+
priority: 10,
594+
pathPattern: '/hello',
595+
targetGroups: [new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 })],
596+
fixedResponse: {
597+
statusCode: '500'
598+
}
599+
}), /`targetGroups`.*`fixedResponse`/);
600+
601+
test.done();
602+
}
485603
};
486604

487605
class ResourceWithLBDependency extends cdk.CfnResource {

0 commit comments

Comments
 (0)