Skip to content

Commit 7b9a07b

Browse files
committed
fix(aws-ecs-patterns): add missing redirectHTTP dependency on default ALB listener
1 parent 2bdc07e commit 7b9a07b

File tree

3 files changed

+190
-14
lines changed

3 files changed

+190
-14
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-ecs-patterns/test/fargate/integ.alb-fargate-service-https.js.snapshot/aws-ecs-integ-alb-fg-https.template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,8 @@
535535
},
536536
"Port": 80,
537537
"Protocol": "HTTP"
538-
}
538+
},
539+
"DependsOn": ["myServiceLBPublicListenerC78AE8A0","myServiceLBPublicListenerECSGroup17E9BBC1"]
539540
},
540541
"myServiceCertificate152F9DDA": {
541542
"Type": "AWS::CertificateManager::Certificate",

packages/aws-cdk-lib/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { IRole } from '../../../aws-iam';
1515
import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '../../../aws-route53';
1616
import { LoadBalancerTarget } from '../../../aws-route53-targets';
17-
import { CfnOutput, Duration, Stack, Token, ValidationError } from '../../../core';
17+
import { Annotations, CfnOutput, Duration, Stack, Token, ValidationError } from '../../../core';
1818

1919
/**
2020
* Describes the type of DNS record the service should create
@@ -530,17 +530,48 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct {
530530
if (this.certificate !== undefined) {
531531
this.listener.addCertificates('Arns', [ListenerCertificate.fromCertificateManager(this.certificate)]);
532532
}
533+
533534
if (props.redirectHTTP) {
534-
this.redirectListener = loadBalancer.addListener('PublicRedirectListener', {
535-
protocol: ApplicationProtocol.HTTP,
536-
port: 80,
537-
open: props.openListener ?? true,
538-
defaultAction: ListenerAction.redirect({
539-
port: props.listenerPort?.toString() || '443',
540-
protocol: ApplicationProtocol.HTTPS,
541-
permanent: true,
542-
}),
543-
});
535+
// Check if the load balancer was created by this construct
536+
if (loadBalancer instanceof ApplicationLoadBalancer) {
537+
// Create a new HTTP listener on port 80 that redirects to HTTPS
538+
this.redirectListener = loadBalancer.addListener('PublicRedirectListener', {
539+
protocol: ApplicationProtocol.HTTP,
540+
port: 80,
541+
open: props.openListener ?? true,
542+
defaultAction: ListenerAction.redirect({
543+
protocol: ApplicationProtocol.HTTPS,
544+
port: props.listenerPort?.toString() || '443',
545+
permanent: true,
546+
}),
547+
});
548+
549+
// Ensure the redirect listener is created after the main listener
550+
this.redirectListener.node.addDependency(this.listener);
551+
552+
// Validate that there are no conflicting port 80 listeners during synth
553+
loadBalancer.node.addValidation({
554+
validate: () => {
555+
const port80Listeners = loadBalancer.listeners.filter(listener =>
556+
listener.port === 80,
557+
);
558+
559+
if (port80Listeners.some(port80Listener => port80Listener !== this.redirectListener)) {
560+
return [
561+
'Cannot automatically configure redirectHTTP: A listener already exists on port 80.',
562+
];
563+
}
564+
return [];
565+
},
566+
});
567+
} else {
568+
// If the ALB was imported then we cannot reliably add the redirect action as we can't find the port 80 listener.
569+
Annotations.of(this).addWarning(
570+
'Cannot automatically configure port 80 HTTP redirect with redirectHTTP: ' +
571+
'The construct cannot reliably determine if a port 80 listener already exists. ' +
572+
'Please configure the redirect manually on the port 80 listener.',
573+
);
574+
}
544575
}
545576

546577
let domainName = loadBalancer.loadBalancerDnsName;

packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/load-balanced-fargate-service.test.ts

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Match, Template } from '../../../assertions';
1+
import { Annotations, Match, Template } from '../../../assertions';
22
import { AutoScalingGroup } from '../../../aws-autoscaling';
33
import { Certificate, CertificateValidation } from '../../../aws-certificatemanager';
44
import * as ec2 from '../../../aws-ec2';
55
import { MachineImage } from '../../../aws-ec2';
66
import * as ecs from '../../../aws-ecs';
77
import { AsgCapacityProvider } from '../../../aws-ecs';
8-
import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer, SslPolicy } from '../../../aws-elasticloadbalancingv2';
8+
import { ApplicationLoadBalancer, ApplicationProtocol, ListenerAction, NetworkLoadBalancer, SslPolicy } from '../../../aws-elasticloadbalancingv2';
99
import * as iam from '../../../aws-iam';
1010
import * as route53 from '../../../aws-route53';
1111
import * as cloudmap from '../../../aws-servicediscovery';
@@ -1111,6 +1111,150 @@ describe('ApplicationLoadBalancedFargateService', () => {
11111111
});
11121112
});
11131113

1114+
test('creates a separate redirect listener when listenerPort is not 80', () => {
1115+
// GIVEN
1116+
const stack = new cdk.Stack();
1117+
const vpc = new ec2.Vpc(stack, 'VPC');
1118+
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });
1119+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' });
1120+
1121+
// WHEN
1122+
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
1123+
cluster,
1124+
taskImageOptions: {
1125+
image: ecs.ContainerImage.fromRegistry('test'),
1126+
},
1127+
domainName: 'api.example.com',
1128+
domainZone: zone,
1129+
protocol: ApplicationProtocol.HTTPS,
1130+
redirectHTTP: true,
1131+
listenerPort: 8443,
1132+
});
1133+
1134+
// THEN
1135+
// Verify that we have two listeners - one for HTTPS and one for HTTP redirect
1136+
Template.fromStack(stack).resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 2);
1137+
1138+
// Verify the HTTPS listener is on port 8443
1139+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
1140+
Port: 8443,
1141+
Protocol: 'HTTPS',
1142+
});
1143+
1144+
// Verify the HTTP redirect listener is on port 80
1145+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
1146+
Port: 80,
1147+
Protocol: 'HTTP',
1148+
DefaultActions: [
1149+
Match.objectLike({
1150+
Type: 'redirect',
1151+
RedirectConfig: {
1152+
Protocol: 'HTTPS',
1153+
Port: '8443',
1154+
StatusCode: 'HTTP_301',
1155+
},
1156+
}),
1157+
],
1158+
});
1159+
1160+
// Verify the dependency between listeners
1161+
expect(service.redirectListener?.node.dependencies[0]).toBe(service.listener);
1162+
});
1163+
1164+
test('throws error when trying to use redirectHTTP with listener on port 80', () => {
1165+
// GIVEN
1166+
const app = new cdk.App();
1167+
const stack = new cdk.Stack(app);
1168+
const vpc = new ec2.Vpc(stack, 'VPC');
1169+
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });
1170+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' });
1171+
1172+
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
1173+
cluster,
1174+
taskImageOptions: {
1175+
image: ecs.ContainerImage.fromRegistry('test'),
1176+
},
1177+
domainName: 'api.example.com',
1178+
domainZone: zone,
1179+
protocol: ApplicationProtocol.HTTPS,
1180+
redirectHTTP: true,
1181+
listenerPort: 80,
1182+
});
1183+
1184+
// THEN
1185+
expect(() => {
1186+
app.synth();
1187+
}).toThrow('Validation failed with the following errors:\n [Default/Service/LB] Cannot automatically configure redirectHTTP: A listener already exists on port 80.');
1188+
});
1189+
1190+
test('adds validation for existing port 80 listeners not owned by the construct', () => {
1191+
// GIVEN
1192+
const app = new cdk.App();
1193+
const stack = new cdk.Stack(app);
1194+
const vpc = new ec2.Vpc(stack, 'VPC');
1195+
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });
1196+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' });
1197+
1198+
// Create a load balancer with an existing port 80 listener
1199+
const lb = new ApplicationLoadBalancer(stack, 'ALB', { vpc, internetFacing: true });
1200+
lb.addListener('ExistingPort80Listener', {
1201+
port: 80,
1202+
protocol: ApplicationProtocol.HTTP,
1203+
defaultAction: ListenerAction.redirect({ port: '1000' }),
1204+
});
1205+
1206+
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
1207+
cluster,
1208+
taskImageOptions: {
1209+
image: ecs.ContainerImage.fromRegistry('test'),
1210+
},
1211+
domainName: 'api.example.com',
1212+
domainZone: zone,
1213+
protocol: ApplicationProtocol.HTTPS,
1214+
redirectHTTP: true,
1215+
loadBalancer: lb,
1216+
});
1217+
// THEN
1218+
expect(() => {
1219+
app.synth();
1220+
}).toThrow('Validation failed with the following errors:\n [Default/ALB] Cannot automatically configure redirectHTTP: A listener already exists on port 80.');
1221+
});
1222+
1223+
test('adds warning for imported load balancers', () => {
1224+
// GIVEN
1225+
const stack = new cdk.Stack();
1226+
const vpc = new ec2.Vpc(stack, 'VPC');
1227+
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });
1228+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' });
1229+
1230+
// Create an imported load balancer
1231+
const importedLb = ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack, 'ImportedALB', {
1232+
loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188',
1233+
securityGroupId: 'sg-123456789',
1234+
loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com',
1235+
loadBalancerCanonicalHostedZoneId: 'some-hosted-zone',
1236+
vpc,
1237+
});
1238+
1239+
// WHEN
1240+
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
1241+
cluster,
1242+
taskImageOptions: {
1243+
image: ecs.ContainerImage.fromRegistry('test'),
1244+
},
1245+
domainName: 'api.example.com',
1246+
domainZone: zone,
1247+
protocol: ApplicationProtocol.HTTPS,
1248+
redirectHTTP: true,
1249+
loadBalancer: importedLb,
1250+
});
1251+
1252+
// THEN
1253+
// Verify that a warning is added
1254+
const annotations = Annotations.fromStack(stack);
1255+
annotations.hasWarning('/Default/Service', 'Cannot automatically configure port 80 HTTP redirect with redirectHTTP: The construct cannot reliably determine if a port 80 listener already exists. Please configure the redirect manually on the port 80 listener.');
1256+
});
1257+
11141258
test('errors when setting HTTPS protocol but not domain name', () => {
11151259
// GIVEN
11161260
const stack = new cdk.Stack();

0 commit comments

Comments
 (0)