diff --git a/API.md b/API.md index 5be1b2ae..81370b9c 100644 --- a/API.md +++ b/API.md @@ -1410,10 +1410,27 @@ const httpLoadBalancerProps: HttpLoadBalancerProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | An ACM certificate to associate with this load balancer. | | requestsPerTarget | number | The number of ALB requests per target. | --- +##### `certificate`Optional + +```typescript +public readonly certificate: ICertificate; +``` + +- *Type:* aws-cdk-lib.aws_certificatemanager.ICertificate +- *Default:* undefined. The load balancer will listen on port 80 over HTTP. + +An ACM certificate to associate with this load balancer. + +If specified, this +extension will listen over HTTPS on port 443. + +--- + ##### `requestsPerTarget`Optional ```typescript diff --git a/src/extensions/http-load-balancer.ts b/src/extensions/http-load-balancer.ts index f6ccab3c..66f8eef1 100644 --- a/src/extensions/http-load-balancer.ts +++ b/src/extensions/http-load-balancer.ts @@ -1,4 +1,5 @@ import { CfnOutput, Duration } from 'aws-cdk-lib'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as alb from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { Construct } from 'constructs'; @@ -10,6 +11,14 @@ export interface HttpLoadBalancerProps { * The number of ALB requests per target. */ readonly requestsPerTarget?: number; + + /** + * An ACM certificate to associate with this load balancer. If specified, this + * extension will listen over HTTPS on port 443. + * + * @default - undefined. The load balancer will listen on port 80 over HTTP. + */ + readonly certificate?: acm.ICertificate; } /** * This extension add a public facing load balancer for sending traffic @@ -19,10 +28,12 @@ export class HttpLoadBalancerExtension extends ServiceExtension { private loadBalancer!: alb.IApplicationLoadBalancer; private listener!: alb.IApplicationListener; private requestsPerTarget?: number; + private certificate?: acm.ICertificate; constructor(props: HttpLoadBalancerProps = {}) { super('load-balancer'); this.requestsPerTarget = props.requestsPerTarget; + this.certificate = props.certificate; } // Before the service is created, go ahead and create the load balancer itself. @@ -33,12 +44,28 @@ export class HttpLoadBalancerExtension extends ServiceExtension { vpc: this.parentService.vpc, internetFacing: true, }); - + const protocol = this.certificate ? alb.ApplicationProtocol.HTTPS : alb.ApplicationProtocol.HTTP; + const port = this.certificate ? 443 : 80; this.listener = this.loadBalancer.addListener(`${this.parentService.id}-listener`, { - port: 80, + port, + protocol, open: true, }); + if (this.certificate) { + this.listener.addCertificates('cert', [alb.ListenerCertificate.fromCertificateManager(this.certificate)]); + this.loadBalancer.addListener(`${this.parentService.id}-redirect-listener`, { + protocol: alb.ApplicationProtocol.HTTP, + port: 80, + open: true, + defaultAction: alb.ListenerAction.redirect({ + port: '443', + protocol: alb.ApplicationProtocol.HTTPS, + permanent: true, + }), + }); + } + // Automatically create an output new CfnOutput(scope, `${this.parentService.id}-load-balancer-dns-output`, { value: this.loadBalancer.loadBalancerDnsName, diff --git a/test/http-load-balancer.test.ts b/test/http-load-balancer.test.ts index f9e7e983..ce07b6c4 100644 --- a/test/http-load-balancer.test.ts +++ b/test/http-load-balancer.test.ts @@ -1,5 +1,6 @@ import { Stack } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import { Container, Environment, HttpLoadBalancerExtension, Service, ServiceDescription } from '../lib'; @@ -138,4 +139,56 @@ describe('http load balancer', () => { }); }).toThrow(/Auto scaling target for the service 'my-service' hasn't been configured. Please use Service construct to configure 'minTaskCount' and 'maxTaskCount'./); }); + + test('should create HTTP listener and redirect if certificate specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + })); + const certificate = acm.Certificate.fromCertificateArn( + stack, + 'importedCert', + 'arn:aws:acm:us-west-2:1234567:certificate/ABC123', + ); + serviceDescription.add(new HttpLoadBalancerExtension({ certificate })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + desiredCount: 2, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { + Port: 443, + Certificates: [ + { CertificateArn: 'arn:aws:acm:us-west-2:1234567:certificate/ABC123' }, + ], + Protocol: 'HTTPS', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { + Port: 80, + Protocol: 'HTTP', + DefaultActions: [ + { + RedirectConfig: { + Port: '443', + Protocol: 'HTTPS', + StatusCode: 'HTTP_301', + }, + Type: 'redirect', + }, + ], + }); + }); }); \ No newline at end of file