Skip to content

Commit

Permalink
feat(ec2): VpcEndpointService construct
Browse files Browse the repository at this point in the history
Add an experimental `VpcEndpointService` construct, to allow exposing Network Load Balancers as endpoints in a VPC.
  • Loading branch information
flemjame-at-amazon authored and mergify[bot] committed Jan 7, 2020
1 parent 24ded60 commit a2713f3
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 5 deletions.
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,16 @@ myEndpoint.connections.allowDefaultPortFromAnyIpv4();

Alternatively, existing security groups can be used by specifying the `securityGroups` prop.

## VPC endpoint services
A VPC endpoint service enables you to expose a Network Load Balancer(s) as a provider service to consumers, who connect to your service over a VPC endpoint. You can restrict access to your service via whitelisted principals (anything that extends ArnPrincipal), and require that new connections be manually accepted.
```ts
new VpcEndpointService(this, "EndpointService", {
vpcEndpointServiceLoadBalancers: [networkLoadBalancer1, networkLoadBalancer2],
acceptanceRequired: true,
whitelistedPrincipals: [new ArnPrincipal("arn:aws:iam::123456789012:root")]
});
```

## Bastion Hosts
A bastion host functions as an instance used to access servers and resources in a VPC without open up the complete VPC on a network level.
You can use bastion hosts using a standard SSH connection targetting port 22 on the host. As an alternative, you can connect the SSH connection
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './vpc';
export * from './vpc-lookup';
export * from './vpn';
export * from './vpc-endpoint';
export * from './vpc-endpoint-service';
export * from './user-data';
export * from './windows-versions';

Expand Down
115 changes: 115 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { ArnPrincipal } from '@aws-cdk/aws-iam';
import { Construct, IResource, Resource } from '@aws-cdk/core';
import { CfnVPCEndpointService, CfnVPCEndpointServicePermissions } from './ec2.generated';

/**
* A load balancer that can host a VPC Endpoint Service
*
*/
export interface IVpcEndpointServiceLoadBalancer {
/**
* The ARN of the load balancer that hosts the VPC Endpoint Service
*/
readonly loadBalancerArn: string;
}

/**
* A VPC endpoint service.
* @experimental
*/
export interface IVpcEndpointService extends IResource {
/**
* Name of the Vpc Endpoint Service
* @experimental
*/
readonly vpcEndpointServiceName?: string;
}

/**
* A VPC endpoint service
* @resource AWS::EC2::VPCEndpointService
* @experimental
*/
export class VpcEndpointService extends Resource implements IVpcEndpointService {

/**
* One or more network load balancer ARNs to host the service.
* @attribute
*/
public readonly vpcEndpointServiceLoadBalancers: IVpcEndpointServiceLoadBalancer[];

/**
* Whether to require manual acceptance of new connections to the service.
* @experimental
*/
public readonly acceptanceRequired: boolean;

/**
* One or more Principal ARNs to allow inbound connections to.
* @experimental
*/
public readonly whitelistedPrincipals: ArnPrincipal[];

private readonly endpointService: CfnVPCEndpointService;

constructor(scope: Construct, id: string, props: VpcEndpointServiceProps) {
super(scope, id);

if (props.vpcEndpointServiceLoadBalancers === undefined || props.vpcEndpointServiceLoadBalancers.length === 0) {
throw new Error("VPC Endpoint Service must have at least one load balancer specified.");
}

this.vpcEndpointServiceLoadBalancers = props.vpcEndpointServiceLoadBalancers;
this.acceptanceRequired = props.acceptanceRequired !== undefined ? props.acceptanceRequired : true;
this.whitelistedPrincipals = props.whitelistedPrincipals !== undefined ? props.whitelistedPrincipals : [];

this.endpointService = new CfnVPCEndpointService(this, id, {
networkLoadBalancerArns: this.vpcEndpointServiceLoadBalancers.map(lb => lb.loadBalancerArn),
acceptanceRequired: this.acceptanceRequired
});

if (this.whitelistedPrincipals.length > 0) {
new CfnVPCEndpointServicePermissions(this, "Permissions", {
serviceId: this.endpointService.ref,
allowedPrincipals: this.whitelistedPrincipals.map(x => x.arn)
});
}
}
}

/**
* Construction properties for a VpcEndpointService.
* @experimental
*/
export interface VpcEndpointServiceProps {

/**
* Name of the Vpc Endpoint Service
* @default - CDK generated name
* @experimental
*/
readonly vpcEndpointServiceName?: string;

/**
* One or more load balancers to host the VPC Endpoint Service.
* @experimental
*/
readonly vpcEndpointServiceLoadBalancers: IVpcEndpointServiceLoadBalancer[];

/**
* Whether requests from service consumers to connect to the service through
* an endpoint must be accepted.
* @default true
* @experimental
*/
readonly acceptanceRequired?: boolean;

/**
* IAM users, IAM roles, or AWS accounts to allow inbound connections from.
* These principals can connect to your service using VPC endpoints. Takes a
* list of one or more ArnPrincipal.
* @default - no principals
* @experimental
*/
readonly whitelistedPrincipals?: ArnPrincipal[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"Resources": {
"MyVpcEndpointServiceWithNoPrincipals9B24276E": {
"Type": "AWS::EC2::VPCEndpointService",
"Properties": {
"NetworkLoadBalancerArns": [
"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a"
],
"AcceptanceRequired": false
}
},
"MyVpcEndpointServiceWithPrincipals41EE2DF2": {
"Type": "AWS::EC2::VPCEndpointService",
"Properties": {
"NetworkLoadBalancerArns": [
"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/1jd81k39sa421ffs"
],
"AcceptanceRequired": false
}
},
"MyVpcEndpointServiceWithPrincipalsPermissions29F9BD5A": {
"Type": "AWS::EC2::VPCEndpointServicePermissions",
"Properties": {
"ServiceId": {
"Ref": "MyVpcEndpointServiceWithPrincipals41EE2DF2"
},
"AllowedPrincipals": [
"arn:aws:iam::123456789012:root"
]
}
}
}
}
46 changes: 46 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ArnPrincipal } from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import * as ec2 from '../lib';

const app = new cdk.App();

/**
* A load balancer that can host a VPC Endpoint Service
*/
class DummyEndpointLoadBalacer implements ec2.IVpcEndpointServiceLoadBalancer {
/**
* The ARN of the load balancer that hosts the VPC Endpoint Service
*/
public readonly loadBalancerArn: string;
constructor(arn: string) {
this.loadBalancerArn = arn;
}
}

class VpcEndpointServiceStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const nlbNoPrincipals = new DummyEndpointLoadBalacer(
"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a");

new ec2.VpcEndpointService(this, "MyVpcEndpointServiceWithNoPrincipals", {
vpcEndpointServiceLoadBalancers: [nlbNoPrincipals],
acceptanceRequired: false,
whitelistedPrincipals: []
});

const nlbWithPrincipals = new DummyEndpointLoadBalacer(
"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/1jd81k39sa421ffs");
const principalArn = new ArnPrincipal("arn:aws:iam::123456789012:root");

new ec2.VpcEndpointService(this, "MyVpcEndpointServiceWithPrincipals", {
vpcEndpointServiceLoadBalancers: [nlbWithPrincipals],
acceptanceRequired: false,
whitelistedPrincipals: [principalArn]
});
}
}

new VpcEndpointServiceStack(app, 'aws-cdk-ec2-vpc-endpoint-service');
app.synth();
106 changes: 106 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect, haveResource } from '@aws-cdk/assert';
import { ArnPrincipal } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
// tslint:disable-next-line:max-line-length
import { IVpcEndpointServiceLoadBalancer, Vpc, VpcEndpointService } from '../lib';

/**
* A load balancer that can host a VPC Endpoint Service
*/
class DummyEndpointLoadBalacer implements IVpcEndpointServiceLoadBalancer {
/**
* The ARN of the load balancer that hosts the VPC Endpoint Service
*/
public readonly loadBalancerArn: string;
constructor(arn: string) {
this.loadBalancerArn = arn;
}
}

export = {
'test vpc endpoint service': {
'create endpoint service with no principals'(test: Test) {
// GIVEN
const stack = new Stack();
new Vpc(stack, "MyVPC");

// WHEN
const lb = new DummyEndpointLoadBalacer("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a");
new VpcEndpointService(stack, "EndpointService", {
vpcEndpointServiceLoadBalancers: [lb],
acceptanceRequired: false,
whitelistedPrincipals: [new ArnPrincipal("arn:aws:iam::123456789012:root")]
});
// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpointService', {
NetworkLoadBalancerArns: ["arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a"],
AcceptanceRequired: false
}));

expect(stack).notTo(haveResource('AWS::EC2::VPCEndpointServicePermissions', {
ServiceId: {
Ref: "EndpointServiceED36BE1F"
},
AllowedPrincipals: []
}));

test.done();
},
'create endpoint service with a principal'(test: Test) {
// GIVEN
const stack = new Stack();

// WHEN
const lb = new DummyEndpointLoadBalacer("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a");
new VpcEndpointService(stack, "EndpointService", {
vpcEndpointServiceLoadBalancers: [lb],
acceptanceRequired: false,
whitelistedPrincipals: [new ArnPrincipal("arn:aws:iam::123456789012:root")]
});

// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpointService', {
NetworkLoadBalancerArns: ["arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a"],
AcceptanceRequired: false
}));

expect(stack).to(haveResource('AWS::EC2::VPCEndpointServicePermissions', {
ServiceId: {
Ref: "EndpointServiceED36BE1F"
},
AllowedPrincipals: ["arn:aws:iam::123456789012:root"]
}));

test.done();
},

'with acceptance requried'(test: Test) {
// GIVEN
const stack = new Stack();

// WHEN
const lb = new DummyEndpointLoadBalacer("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a");
new VpcEndpointService(stack, "EndpointService", {
vpcEndpointServiceLoadBalancers: [lb],
acceptanceRequired: true,
whitelistedPrincipals: [new ArnPrincipal("arn:aws:iam::123456789012:root")]
});

// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpointService', {
NetworkLoadBalancerArns: ["arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a"],
AcceptanceRequired: true
}));

expect(stack).to(haveResource('AWS::EC2::VPCEndpointServicePermissions', {
ServiceId: {
Ref: "EndpointServiceED36BE1F"
},
AllowedPrincipals: ["arn:aws:iam::123456789012:root"]
}));

test.done();
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,7 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
/**
* A network load balancer
*/
export interface INetworkLoadBalancer extends ILoadBalancerV2 {
/**
* The ARN of this load balancer
*/
readonly loadBalancerArn: string;
export interface INetworkLoadBalancer extends ILoadBalancerV2, ec2.IVpcEndpointServiceLoadBalancer {

/**
* The VPC this load balancer has been created in (if available)
Expand Down

0 comments on commit a2713f3

Please sign in to comment.