Skip to content

Commit

Permalink
feat(ecs): VPC link for API Gatweay and ECS services (#1541)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
dotxlem authored and rix0rrr committed Jan 16, 2019
1 parent ff96f3f commit 6642ca2
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 146 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/integration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import iam = require('@aws-cdk/aws-iam');
import { Method } from './method';
import { VpcLink } from './vpc-link';

export interface IntegrationOptions {
/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,6 +39,7 @@ export interface MethodOptions {
// - RequestModels
// - RequestParameters
// - MethodResponses
requestParameters?: { [param: string]: boolean };
}

export interface MethodProps {
Expand Down Expand Up @@ -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)
};

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
};
}
Expand Down
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 4 additions & 1 deletion packages/@aws-cdk/aws-apigateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -79,4 +82,4 @@
"resource-attribute:@aws-cdk/aws-apigateway.IRestApi.restApiRootResourceId"
]
}
}
}
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/test.method.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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();
}
};
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts
Original file line number Diff line number Diff line change
@@ -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();
},
};
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ecs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
69 changes: 5 additions & 64 deletions packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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', {});

Expand All @@ -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);
}
}
Loading

0 comments on commit 6642ca2

Please sign in to comment.