From 6642ca2341716dcf0477ba774c621c11d6bef6aa Mon Sep 17 00:00:00 2001 From: dotxlem <46094829+dotxlem@users.noreply.github.com> Date: Wed, 16 Jan 2019 05:32:35 -0330 Subject: [PATCH] feat(ecs): VPC link for API Gatweay and ECS services (#1541) Overview ======== The primary purpose of this work is to fill in the gaps in implementation for deploying a VPC link between API Gateway, and an ECS service. My goal was to allow setting up a {proxy+} API which would forward to a Fargate service in a private VPC. This has been tagged as 'ecs', but also involves changes to api gateway. Since VPC links require an NLB, the LoadBalanced{Fargate|Ecs}Service classes have been modified to support selecting either an ALB or an NLB. Changes ======= On the APIGW side, `IntegrationOptions` now accepts an optional connetion type enum, as well as a VpcLink. `VpcLink` itself is a new construct which accepts an array of Network Load Balancers. I also added the missing `requestParameters` prop for `Method`, to allow properly setting up a proxy path variable. For ECS, in my use case I wanted to use the LoadBalanced*Service constructs, however they only supported ALB. I have pulled all of the ELBv2 related setup into the new `LoadBalancedService` base class, and also created a base props interface `LoadBalancedServiceProps`. This deals with the common setup between the Fargate and ECS services, and allows the selection of ALB or NLB. As a side-effect of this refactoring, you can also now pass a Certificate to `LoadBalancedEcsService`. There is a new `Method` test for the VPC link props, as well as new tests for both `VpcLink` and `LoadBalancedFargateService`. --- packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../aws-apigateway/lib/integration.ts | 25 ++++ .../@aws-cdk/aws-apigateway/lib/method.ts | 14 +- .../@aws-cdk/aws-apigateway/lib/vpc-link.ts | 49 +++++++ packages/@aws-cdk/aws-apigateway/package.json | 5 +- .../aws-apigateway/test/test.method.ts | 49 +++++++ .../aws-apigateway/test/test.vpc-link.ts | 31 ++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../aws-ecs/lib/load-balanced-ecs-service.ts | 69 +-------- .../lib/load-balanced-fargate-service.ts | 86 +---------- .../aws-ecs/lib/load-balanced-service-base.ts | 134 ++++++++++++++++++ .../test.load-balanced-fargate-service.ts | 51 +++++++ 12 files changed, 369 insertions(+), 146 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-service-base.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.load-balanced-fargate-service.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 4cce185dd3fe0..1ccd62b520805 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -6,6 +6,7 @@ export * from './deployment'; export * from './stage'; export * from './integrations'; export * from './lambda-api'; +export * from './vpc-link'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 294f9d0f5fc6c..3214f931da67a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -1,5 +1,6 @@ import iam = require('@aws-cdk/aws-iam'); import { Method } from './method'; +import { VpcLink } from './vpc-link'; export interface IntegrationOptions { /** @@ -93,6 +94,18 @@ export interface IntegrationOptions { * @see http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html */ selectionPattern?: string; + + /** + * The type of network connection to the integration endpoint. + * @default ConnectionType.Internet + */ + connectionType?: ConnectionType; + + /** + * The VpcLink used for the integration. + * Required if connectionType is VPC_LINK + */ + vpcLink?: VpcLink; } export interface IntegrationProps { @@ -217,6 +230,18 @@ export enum PassthroughBehavior { WhenNoTemplates = 'WHEN_NO_TEMPLATES' } +export enum ConnectionType { + /** + * For connections through the public routable internet + */ + Internet = 'INTERNET', + + /** + * For private connections between API Gateway and a network load balancer in a VPC + */ + VpcLink = 'VPC_LINK' +} + export interface IntegrationResponse { /** * The status code that API Gateway uses to map the integration response to diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 3822c56ee85e3..cb263bc5083cc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { CfnMethod, CfnMethodProps } from './apigateway.generated'; -import { Integration } from './integration'; +import { ConnectionType, Integration } from './integration'; import { MockIntegration } from './integrations/mock'; import { IRestApiResource } from './resource'; import { RestApi } from './restapi'; @@ -39,6 +39,7 @@ export interface MethodOptions { // - RequestModels // - RequestParameters // - MethodResponses + requestParameters?: { [param: string]: boolean }; } export interface MethodProps { @@ -91,6 +92,7 @@ export class Method extends cdk.Construct { apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired, authorizationType: options.authorizationType || defaultMethodOptions.authorizationType || AuthorizationType.None, authorizerId: options.authorizerId || defaultMethodOptions.authorizerId, + requestParameters: options.requestParameters, integration: this.renderIntegration(props.integration) }; @@ -154,6 +156,14 @@ export class Method extends cdk.Construct { throw new Error(`'credentialsPassthrough' and 'credentialsRole' are mutually exclusive`); } + if (options.connectionType === ConnectionType.VpcLink && options.vpcLink === undefined) { + throw new Error(`'connectionType' of VPC_LINK requires 'vpcLink' prop to be set`); + } + + if (options.connectionType === ConnectionType.Internet && options.vpcLink !== undefined) { + throw new Error(`cannot set 'vpcLink' where 'connectionType' is INTERNET`); + } + if (options.credentialsRole) { credentials = options.credentialsRole.roleArn; } else if (options.credentialsPassthrough) { @@ -173,6 +183,8 @@ export class Method extends cdk.Construct { requestTemplates: options.requestTemplates, passthroughBehavior: options.passthroughBehavior, integrationResponses: options.integrationResponses, + connectionType: options.connectionType, + connectionId: options.vpcLink ? options.vpcLink.vpcLinkId : undefined, credentials, }; } diff --git a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts new file mode 100644 index 0000000000000..73584be5c5387 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts @@ -0,0 +1,49 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { CfnVpcLink } from './apigateway.generated'; + +/** + * Properties for a VpcLink + */ +export interface VpcLinkProps { + /** + * The name used to label and identify the VPC link. + * @default automatically generated name + */ + name?: string; + + /** + * The description of the VPC link. + * @default no description + */ + description?: string; + + /** + * The network load balancers of the VPC targeted by the VPC link. + * The network load balancers must be owned by the same AWS account of the API owner. + */ + targets: elbv2.INetworkLoadBalancer[]; +} + +/** + * Define a new VPC Link + * Specifies an API Gateway VPC link for a RestApi to access resources in an Amazon Virtual Private Cloud (VPC). + */ +export class VpcLink extends cdk.Construct { + /** + * Physical ID of the VpcLink resource + */ + public readonly vpcLinkId: string; + + constructor(scope: cdk.Construct, id: string, props: VpcLinkProps) { + super(scope, id); + + const cfnResource = new CfnVpcLink(this, 'Resource', { + name: props.name || this.node.uniqueId, + description: props.description, + targetArns: props.targets.map(nlb => nlb.loadBalancerArn) + }); + + this.vpcLinkId = cfnResource.vpcLinkId; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 3d4cdaa5ee95d..036da9b081567 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -55,6 +55,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.22.0", + "@aws-cdk/aws-ec2": "^0.22.0", "cdk-build-tools": "^0.22.0", "cdk-integ-tools": "^0.22.0", "cfn2ts": "^0.22.0", @@ -63,12 +64,14 @@ "dependencies": { "@aws-cdk/aws-iam": "^0.22.0", "@aws-cdk/aws-lambda": "^0.22.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0", "@aws-cdk/cdk": "^0.22.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-iam": "^0.22.0", "@aws-cdk/aws-lambda": "^0.22.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0", "@aws-cdk/cdk": "^0.22.0" }, "engines": { @@ -79,4 +82,4 @@ "resource-attribute:@aws-cdk/aws-apigateway.IRestApi.restApiRootResourceId" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index 2a377a139f661..645d1f9f9f398 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -1,8 +1,11 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import apigateway = require('../lib'); +import { ConnectionType } from '../lib'; export = { 'default setup'(test: Test) { @@ -254,4 +257,50 @@ export = { test.throws(() => api.root.addMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); test.done(); }, + + 'integration connectionType VpcLink requires vpcLink to be set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + + // WHEN + const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.HttpProxy, + integrationHttpMethod: 'ANY', + options: { + connectionType: ConnectionType.VpcLink, + } + }); + + // THEN + test.throws(() => api.root.addMethod('GET', integration), /'connectionType' of VPC_LINK requires 'vpcLink' prop to be set/); + test.done(); + }, + + 'connectionType of INTERNET and vpcLink are mutually exclusive'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { + vpc + }); + const link = new apigateway.VpcLink(stack, 'link', { + targets: [nlb] + }); + + // WHEN + const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.HttpProxy, + integrationHttpMethod: 'ANY', + options: { + connectionType: ConnectionType.Internet, + vpcLink: link + } + }); + + // THEN + test.throws(() => api.root.addMethod('GET', integration), /cannot set 'vpcLink' where 'connectionType' is INTERNET/); + test.done(); + } }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts b/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts new file mode 100644 index 0000000000000..fb72e0909a315 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts @@ -0,0 +1,31 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { + vpc + }); + + // WHEN + new apigateway.VpcLink(stack, 'VpcLink', { + name: 'MyLink', + targets: [nlb] + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApiGateway::VpcLink', { + Name: "MyLink", + TargetArns: [ { Ref: "NLB55158F82" } ] + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 902efcdc04529..15efbab29225d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -13,6 +13,7 @@ export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; export * from './linux-parameters'; +export * from './load-balanced-service-base'; export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-ecs-service'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 8f227b4fe564a..4d021fac92f19 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -1,24 +1,12 @@ -import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); -import { ICluster } from './cluster'; -import { IContainerImage } from './container-image'; import { Ec2Service } from './ec2/ec2-service'; import { Ec2TaskDefinition } from './ec2/ec2-task-definition'; +import { LoadBalancedServiceBase, LoadBalancedServiceBaseProps } from './load-balanced-service-base'; /** * Properties for a LoadBalancedEc2Service */ -export interface LoadBalancedEc2ServiceProps { - /** - * The cluster where your EC2 service will be deployed - */ - cluster: ICluster; - - /** - * The image to start. - */ - image: IContainerImage; - +export interface LoadBalancedEc2ServiceProps extends LoadBalancedServiceBaseProps { /** * The hard limit (in MiB) of memory to present to the container. * @@ -40,47 +28,14 @@ export interface LoadBalancedEc2ServiceProps { * At least one of memoryLimitMiB and memoryReservationMiB is required. */ memoryReservationMiB?: number; - - /** - * The container port of the application load balancer attached to your EC2 service. Corresponds to container port mapping. - * - * @default 80 - */ - containerPort?: number; - - /** - * Determines whether the Application Load Balancer will be internet-facing - * - * @default true - */ - publicLoadBalancer?: boolean; - - /** - * Number of desired copies of running tasks - * - * @default 1 - */ - desiredCount?: number; - - /** - * Environment variables to pass to the container - * - * @default No environment variables - */ - environment?: { [key: string]: string }; } /** * A single task running on an ECS cluster fronted by a load balancer */ -export class LoadBalancedEc2Service extends cdk.Construct { - /** - * The load balancer that is fronting the ECS service - */ - public readonly loadBalancer: elbv2.ApplicationLoadBalancer; - +export class LoadBalancedEc2Service extends LoadBalancedServiceBase { constructor(scope: cdk.Construct, id: string, props: LoadBalancedEc2ServiceProps) { - super(scope, id); + super(scope, id, props); const taskDefinition = new Ec2TaskDefinition(this, 'TaskDef', {}); @@ -100,20 +55,6 @@ export class LoadBalancedEc2Service extends cdk.Construct { taskDefinition, }); - const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; - const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { - vpc: props.cluster.vpc, - internetFacing - }); - - this.loadBalancer = lb; - - const listener = lb.addListener('PublicListener', { port: 80, open: true }); - listener.addTargets('ECS', { - port: 80, - targets: [service] - }); - - new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); + this.addServiceAsTarget(service); } } 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 3124544781387..f1eee7befeaf1 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,27 +1,14 @@ -import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import { AliasRecord, IHostedZone } from '@aws-cdk/aws-route53'; import cdk = require('@aws-cdk/cdk'); -import { ICluster } from './cluster'; -import { IContainerImage } from './container-image'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; +import { LoadBalancedServiceBase, LoadBalancedServiceBaseProps } from './load-balanced-service-base'; import { AwsLogDriver } from './log-drivers/aws-log-driver'; /** * Properties for a LoadBalancedEcsService */ -export interface LoadBalancedFargateServiceProps { - /** - * The cluster where your Fargate service will be deployed - */ - cluster: ICluster; - - /** - * The image to start - */ - image: IContainerImage; - +export interface LoadBalancedFargateServiceProps extends LoadBalancedServiceBaseProps { /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -59,20 +46,6 @@ export interface LoadBalancedFargateServiceProps { */ memoryMiB?: string; - /** - * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. - * - * @default 80 - */ - containerPort?: number; - - /** - * Determines whether the Application Load Balancer will be internet-facing - * - * @default true - */ - publicLoadBalancer?: boolean; - /** * Determines whether your Fargate Service will be assigned a public IP address. * @@ -80,13 +53,6 @@ export interface LoadBalancedFargateServiceProps { */ publicTasks?: boolean; - /** - * Number of desired copies of running tasks - * - * @default 1 - */ - desiredCount?: number; - /* * Domain name for the service, e.g. api.example.com */ @@ -97,39 +63,23 @@ export interface LoadBalancedFargateServiceProps { */ domainZone?: IHostedZone; - /** - * Certificate Manager certificate to associate with the load balancer. - * Setting this option will set the load balancer port to 443. - */ - certificate?: ICertificate; /** * Whether to create an AWS log driver * * @default true */ createLogs?: boolean; - - /** - * Environment variables to pass to the container - * - * @default No environment variables - */ - environment?: { [key: string]: string }; - } /** * A Fargate service running on an ECS cluster fronted by a load balancer */ -export class LoadBalancedFargateService extends cdk.Construct { - public readonly loadBalancer: elbv2.ApplicationLoadBalancer; - - public readonly targetGroup: elbv2.ApplicationTargetGroup; +export class LoadBalancedFargateService extends LoadBalancedServiceBase { public readonly service: FargateService; constructor(scope: cdk.Construct, id: string, props: LoadBalancedFargateServiceProps) { - super(scope, id); + super(scope, id, props); const taskDefinition = new FargateTaskDefinition(this, 'TaskDef', { memoryMiB: props.memoryMiB, @@ -157,31 +107,7 @@ export class LoadBalancedFargateService extends cdk.Construct { }); this.service = service; - const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; - const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { - vpc: props.cluster.vpc, - internetFacing - }); - - this.loadBalancer = lb; - - 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 }); - } - - this.targetGroup = listener.addTargets('ECS', { - port: 80, - targets: [service] - }); - - new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); + this.addServiceAsTarget(service); if (typeof props.domainName !== 'undefined') { if (typeof props.domainZone === 'undefined') { @@ -191,7 +117,7 @@ export class LoadBalancedFargateService extends cdk.Construct { new AliasRecord(this, "DNS", { zone: props.domainZone, recordName: props.domainName, - target: lb + target: this.loadBalancer }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-service-base.ts new file mode 100644 index 0000000000000..ab521e8deec1a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-service-base.ts @@ -0,0 +1,134 @@ +import { ICertificate } from '@aws-cdk/aws-certificatemanager'; +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { BaseService } from './base/base-service'; +import { ICluster } from './cluster'; +import { IContainerImage } from './container-image'; + +export enum LoadBalancerType { + Application, + Network +} + +export interface LoadBalancedServiceBaseProps { + /** + * The cluster where your service will be deployed + */ + cluster: ICluster; + + /** + * The image to start. + */ + image: IContainerImage; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; + + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; + + /** + * Whether to create an application load balancer or a network load balancer + * @default application + */ + loadBalancerType?: LoadBalancerType + + /** + * Certificate Manager certificate to associate with the load balancer. + * Setting this option will set the load balancer port to 443. + */ + certificate?: ICertificate; + + /** + * Environment variables to pass to the container + * + * @default No environment variables + */ + environment?: { [key: string]: string }; +} + +/** + * Base class for load-balanced Fargate and ECS service + */ +export abstract class LoadBalancedServiceBase extends cdk.Construct { + public readonly loadBalancerType: LoadBalancerType; + + public readonly loadBalancer: elbv2.BaseLoadBalancer; + + public readonly listener: elbv2.ApplicationListener | elbv2.NetworkListener; + + public readonly targetGroup: elbv2.ApplicationTargetGroup | elbv2.NetworkTargetGroup; + + constructor(scope: cdk.Construct, id: string, props: LoadBalancedServiceBaseProps) { + super(scope, id); + + // Load balancer + this.loadBalancerType = props.loadBalancerType !== undefined ? props.loadBalancerType : LoadBalancerType.Application; + + if (this.loadBalancerType !== LoadBalancerType.Application && this.loadBalancerType !== LoadBalancerType.Network) { + throw new Error(`invalid loadBalancerType`); + } + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + + const lbProps = { + vpc: props.cluster.vpc, + internetFacing + }; + + if (this.loadBalancerType === LoadBalancerType.Application) { + this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', lbProps); + } else { + this.loadBalancer = new elbv2.NetworkLoadBalancer(this, 'LB', lbProps); + } + + const targetProps = { + port: 80 + }; + + const hasCertificate = props.certificate !== undefined; + if (hasCertificate && this.loadBalancerType !== LoadBalancerType.Application) { + throw new Error("Cannot add certificate to an NLB"); + } + + if (this.loadBalancerType === LoadBalancerType.Application) { + this.listener = (this.loadBalancer as elbv2.ApplicationLoadBalancer).addListener('PublicListener', { + port: hasCertificate ? 443 : 80, + open: true + }); + this.targetGroup = this.listener.addTargets('ECS', targetProps); + + if (props.certificate !== undefined) { + this.listener.addCertificateArns('Arns', [props.certificate.certificateArn]); + } + } else { + this.listener = (this.loadBalancer as elbv2.NetworkLoadBalancer).addListener('PublicListener', { port: 80 }); + this.targetGroup = this.listener.addTargets('ECS', targetProps); + } + + new cdk.Output(this, 'LoadBalancerDNS', { value: this.loadBalancer.dnsName }); + } + + protected addServiceAsTarget(service: BaseService) { + if (this.loadBalancerType === LoadBalancerType.Application) { + (this.targetGroup as elbv2.ApplicationTargetGroup).addTarget(service); + } else { + (this.targetGroup as elbv2.NetworkTargetGroup).addTarget(service); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/test.load-balanced-fargate-service.ts new file mode 100644 index 0000000000000..cdb28604975be --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.load-balanced-fargate-service.ts @@ -0,0 +1,51 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + 'certificate requires an application load balancer'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + const cert = new Certificate(stack, 'Cert', { domainName: '*.example.com' }); + const toThrow = () => { + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + certificate: cert, + loadBalancerType: ecs.LoadBalancerType.Network, + image: ecs.ContainerImage.fromDockerHub("/aws/aws-example-app") + }); + }; + + // THEN + test.throws(() => toThrow(), /Cannot add certificate to an NLB/); + test.done(); + }, + + 'setting loadBalancerType to Network creates an NLB'(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, + loadBalancerType: ecs.LoadBalancerType.Network, + image: ecs.ContainerImage.fromDockerHub("/aws/aws-example-app") + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Type: 'network' + })); + + test.done(); + } +}; \ No newline at end of file