From 4ff33c634e742803388b176a64ae219723321298 Mon Sep 17 00:00:00 2001 From: Clare Liguori Date: Wed, 7 Nov 2018 22:51:16 -0800 Subject: [PATCH] feat(aws-ecs): Support HTTPS in load balanced Fargate service --- .../lib/load-balanced-fargate-service.ts | 49 ++++++++++++- packages/@aws-cdk/aws-ecs/package.json | 1 + packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 69 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 2287296468ea4..82d3b0f3095e4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,4 +1,5 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import route53 = require('@aws-cdk/aws-route53'); import cdk = require('@aws-cdk/cdk'); import { ICluster } from './cluster'; import { IContainerImage } from './container-image'; @@ -76,6 +77,22 @@ export interface LoadBalancedFargateServiceProps { * @default false */ publicTasks?: boolean; + + /** + * Domain name for the service, e.g. api.example.com + */ + domainName?: string; + + /** + * Route53 hosted zone name for the domain, e.g. "example.com." + */ + domainZone?: string; + + /** + * ARN of the Certificate Manager SSL certificate to associate with the load balancer. + * Setting this option will set the load balancer port to 443. + */ + certificateArn?: string; } /** @@ -115,12 +132,42 @@ export class LoadBalancedFargateService extends cdk.Construct { this.loadBalancer = lb; - const listener = lb.addListener('PublicListener', { port: 80, open: true }); + let listener; + if (props.certificateArn) { + // TODO once ALB redirects are supported in CloudFormation, + // redirect http to https + listener = lb.addListener('PublicListener', { + port: 443, + open: true, + certificateArns: [props.certificateArn] + }); + } else { + listener = lb.addListener('PublicListener', { port: 80, open: true }); + } + listener.addTargets('ECS', { port: 80, targets: [service] }); new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); + + if (props.domainName) { + if (!props.domainZone) { + throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + } + + new route53.cloudformation.RecordSetGroupResource(this, "DNS", { + hostedZoneName: props.domainZone, + recordSets: [{ + name: props.domainName, + type: 'A', + aliasTarget: { + hostedZoneId: lb.canonicalHostedZoneId, + dnsName: lb.dnsName + } + }] + }); + } } } diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index d76d1e87e9cc5..75bc8c7116a31 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -72,6 +72,7 @@ "@aws-cdk/aws-iam": "^0.15.1", "@aws-cdk/aws-lambda": "^0.15.1", "@aws-cdk/aws-logs": "^0.15.1", + "@aws-cdk/aws-route53": "^0.15.1", "@aws-cdk/cdk": "^0.15.1", "@aws-cdk/cx-api": "^0.15.1" }, diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 900116a34fdb9..1bd66e7b8d612 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -40,6 +40,75 @@ export = { // THEN - stack containers a load balancer expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Port: 80 + })); + + test.done(); + }, + + 'test Fargateloadbalanced construct with TLS'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test'), + domainName: 'api.example.com', + domainZone: 'example.com.', + certificateArn: 'helloworld' + }); + + // THEN - stack contains a load balancer and a service + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + Port: 443, + Certificates: [{ + CertificateArn: "helloworld" + }] + })); + + expect(stack).to(haveResource("AWS::ECS::Service", { + DesiredCount: 1, + LaunchType: "FARGATE", + })); + + expect(stack).to(haveResource('AWS::Route53::RecordSetGroup', { + HostedZoneName: "example.com.", + RecordSets: [ + { + Name: 'api.example.com', + Type: 'A', + AliasTarget: { + HostedZoneId: { 'Fn::GetAtt': [ 'ServiceLBE9A1ADBC', 'CanonicalHostedZoneID' ] }, + DNSName: { 'Fn::GetAtt': [ 'ServiceLBE9A1ADBC', 'DNSName' ] }, + } + } + ] + })); + + test.done(); + }, + + "errors when setting domainName but not domainZone"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // THEN + test.throws(() => { + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test'), + domainName: 'api.example.com' + }); + }); + test.done(); } };