From 76a5cc78b5618f03a57c22deac6fd378d56d372f Mon Sep 17 00:00:00 2001 From: Clare Liguori Date: Wed, 14 Nov 2018 04:37:11 -0800 Subject: [PATCH] feat(aws-ecs): Support HTTPS in load balanced Fargate service (#1115) --- .../lib/load-balanced-fargate-service.ts | 41 ++++++++++- packages/@aws-cdk/aws-ecs/package.json | 4 +- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 70 +++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) 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 5ee7bba7769b3..722a016793def 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,6 @@ +import { CertificateRef } from '@aws-cdk/aws-certificatemanager'; import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import { AliasRecord, HostedZoneRef } from '@aws-cdk/aws-route53'; import cdk = require('@aws-cdk/cdk'); import { ICluster } from './cluster'; import { IContainerImage } from './container-image'; @@ -83,6 +85,22 @@ export interface LoadBalancedFargateServiceProps { * @default 1 */ desiredCount?: number; + + /* + * Domain name for the service, e.g. api.example.com + */ + domainName?: string; + + /** + * Route53 hosted zone for the domain, e.g. "example.com." + */ + domainZone?: HostedZoneRef; + + /** + * Certificate Manager certificate to associate with the load balancer. + * Setting this option will set the load balancer port to 443. + */ + certificate?: CertificateRef; } /** @@ -123,12 +141,33 @@ export class LoadBalancedFargateService extends cdk.Construct { this.loadBalancer = lb; - const listener = lb.addListener('PublicListener', { port: 80, open: true }); + let listener; + if (typeof props.certificate !== 'undefined') { + listener = lb.addListener('PublicListener', { + port: 443, + open: true, + certificateArns: [props.certificate.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 (typeof props.domainName !== 'undefined') { + if (typeof props.domainZone === 'undefined') { + throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); + } + + new AliasRecord(props.domainZone, "DNS", { + recordName: props.domainName, + target: lb + }); + } } } diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 06e688e523bed..92ee16c61b6a2 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -63,6 +63,7 @@ "dependencies": { "@aws-cdk/aws-applicationautoscaling": "^0.17.0", "@aws-cdk/aws-autoscaling": "^0.17.0", + "@aws-cdk/aws-certificatemanager": "^0.17.0", "@aws-cdk/aws-cloudformation": "^0.17.0", "@aws-cdk/aws-cloudwatch": "^0.17.0", "@aws-cdk/aws-ec2": "^0.17.0", @@ -72,6 +73,7 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/aws-lambda": "^0.17.0", "@aws-cdk/aws-logs": "^0.17.0", + "@aws-cdk/aws-route53": "^0.17.0", "@aws-cdk/cdk": "^0.17.0", "@aws-cdk/cx-api": "^0.17.0" }, @@ -88,4 +90,4 @@ "@aws-cdk/aws-logs": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 3d9f6d29dc599..048ef2593689d 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -1,5 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import { CertificateRef } from '@aws-cdk/aws-certificatemanager'; import ec2 = require('@aws-cdk/aws-ec2'); +import { PublicHostedZone } from '@aws-cdk/aws-route53'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -52,6 +54,74 @@ export = { LaunchType: "FARGATE", })); + 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 }); + const zone = new PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' }); + + // WHEN + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromDockerHub('test'), + domainName: 'api.example.com', + domainZone: zone, + certificate: CertificateRef.import(stack, 'Cert', { 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::RecordSet', { + Name: 'api.example.com.', + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + 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.ContainerImage.fromDockerHub('test'), + domainName: 'api.example.com' + }); + }); + test.done(); },