Skip to content

Commit

Permalink
feat(ecs): add protocol option and default certificate for HTTPS serv…
Browse files Browse the repository at this point in the history
…ices (#4120)

* feat(ecs): add protocol option and default certificate for HTTPS services

* swap protocol validation logic
  • Loading branch information
clareliguori authored and mergify[bot] committed Sep 25, 2019
1 parent f11bece commit e02c6cc
Show file tree
Hide file tree
Showing 4 changed files with 887 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager';
import { IVpc } from '@aws-cdk/aws-ec2';
import { AwsLogDriver, BaseService, Cluster, ContainerImage, ICluster, LogDriver, Secret } from '@aws-cdk/aws-ecs';
import { ApplicationListener, ApplicationLoadBalancer, ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2';
import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2';
import { IRole } from '@aws-cdk/aws-iam';
import { AddressRecordTarget, ARecord, IHostedZone } from '@aws-cdk/aws-route53';
import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets';
Expand Down Expand Up @@ -62,9 +62,11 @@ export interface ApplicationLoadBalancedServiceBaseProps {

/**
* Certificate Manager certificate to associate with the load balancer.
* Setting this option will set the load balancer port to 443.
* Setting this option will set the load balancer protocol to HTTPS.
*
* @default - No certificate associated with the load balancer.
* @default - No certificate associated with the load balancer, if using
* the HTTP protocol. For HTTPS, a DNS-validated certificate will be
* created for the load balancer's specified domain name.
*/
readonly certificate?: ICertificate;

Expand All @@ -89,6 +91,17 @@ export interface ApplicationLoadBalancedServiceBaseProps {
*/
readonly enableLogging?: boolean;

/**
* The protocol for connections from clients to the load balancer.
* The load balancer port is determined from the protocol (port 80 for
* HTTP, port 443 for HTTPS). A domain name and zone must be also be
* specified if using HTTPS.
*
* @default HTTP. If a certificate is specified, the protocol will be
* set by default to HTTPS.
*/
readonly protocol?: ApplicationProtocol;

/**
* The domain name for the service, e.g. "api.example.com."
*
Expand Down Expand Up @@ -162,6 +175,9 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct {
public readonly listener: ApplicationListener;

public readonly targetGroup: ApplicationTargetGroup;

public readonly certificate: ICertificate;

/**
* The cluster that hosts the service.
*/
Expand Down Expand Up @@ -199,14 +215,33 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct {
port: 80
};

if (props.certificate !== undefined && props.protocol !== undefined && props.protocol !== ApplicationProtocol.HTTPS) {
throw new Error('The HTTPS protocol must be used when a certificate is given');
}
const protocol = props.protocol !== undefined ? props.protocol : (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP);

this.listener = this.loadBalancer.addListener('PublicListener', {
port: props.certificate !== undefined ? 443 : 80,
protocol,
open: true
});
this.targetGroup = this.listener.addTargets('ECS', targetProps);

if (props.certificate !== undefined) {
this.listener.addCertificateArns('Arns', [props.certificate.certificateArn]);
if (protocol === ApplicationProtocol.HTTPS) {
if (typeof props.domainName === 'undefined' || typeof props.domainZone === 'undefined') {
throw new Error('A domain name and zone is required when using the HTTPS protocol');
}

if (props.certificate !== undefined) {
this.certificate = props.certificate;
} else {
this.certificate = new DnsValidatedCertificate(this, 'Certificate', {
domainName: props.domainName,
hostedZone: props.domainZone
});
}
}
if (this.certificate !== undefined) {
this.listener.addCertificateArns('Arns', [this.certificate.certificateArn]);
}

if (typeof props.domainName !== 'undefined') {
Expand Down
106 changes: 105 additions & 1 deletion packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Certificate } from '@aws-cdk/aws-certificatemanager';
import ec2 = require('@aws-cdk/aws-ec2');
import ecs = require('@aws-cdk/aws-ecs');
import { AwsLogDriver } from '@aws-cdk/aws-ecs';
import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2';
import { PublicHostedZone } from '@aws-cdk/aws-route53';
import cdk = require('@aws-cdk/core');
import { Test } from 'nodeunit';
Expand Down Expand Up @@ -176,7 +177,8 @@ export = {
}));

expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
Port: 80
Port: 80,
Protocol: 'HTTP'
}));

test.done();
Expand Down Expand Up @@ -249,6 +251,7 @@ export = {

expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
Port: 443,
Protocol: 'HTTPS',
Certificates: [{
CertificateArn: "helloworld"
}]
Expand All @@ -274,6 +277,69 @@ export = {
test.done();
},

'test Fargateloadbalanced construct with TLS and default certificate'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });
const zone = new PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' });

// WHEN
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
cluster,
image: ecs.ContainerImage.fromRegistry('test'),
domainName: 'api.example.com',
domainZone: zone,
protocol: ApplicationProtocol.HTTPS
});

// THEN - stack contains a load balancer, a service, and a certificate
expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', {
ServiceToken: {
'Fn::GetAtt': [
'ServiceCertificateCertificateRequestorFunctionB69CD117',
'Arn'
]
},
DomainName: 'api.example.com',
HostedZoneId: {
Ref: "HostedZoneDB99F866"
}
}));

expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer'));

expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
Port: 443,
Protocol: 'HTTPS',
Certificates: [{
CertificateArn: { 'Fn::GetAtt': [
'ServiceCertificateCertificateRequestorResource0FC297E9',
'Arn'
]}
}]
}));

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();
Expand All @@ -291,6 +357,44 @@ export = {

test.done();
},

'errors when setting both HTTP protocol and certificate'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });

// THEN
test.throws(() => {
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
cluster,
image: ecs.ContainerImage.fromRegistry('test'),
protocol: ApplicationProtocol.HTTP,
certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld')
});
});

test.done();
},

'errors when setting HTTPS protocol but not domain name'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'VPC');
const cluster = new ecs.Cluster(stack, 'Cluster', { vpc });

// THEN
test.throws(() => {
new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
cluster,
image: ecs.ContainerImage.fromRegistry('test'),
protocol: ApplicationProtocol.HTTPS
});
});

test.done();
},

'test Fargate loadbalanced construct with optional log driver input'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down

0 comments on commit e02c6cc

Please sign in to comment.