Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ec2): add support for vpn connections #1899

Merged
merged 14 commits into from
Mar 4, 2019
38 changes: 38 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,41 @@ selectable by instantiating one of these classes:
> section of your `cdk.json`.
>
> We will add command-line options to make this step easier in the future.

### VPN connections to a VPC

Create your VPC with VPN connections by specifying the `vpnConnections` props (keys are construct `id`s):

```ts
const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {
vpnConnections: {
dynamic: { // Dynamic routing (BGP)
ip: '1.2.3.4'
},
static: { // Static routing
ip: '4.5.6.7',
staticRoutes: [
'192.168.10.0/24',
'192.168.20.0/24'
]
}
}
});
```

To create a VPC that can accept VPN connections, set `vpnGateway` to `true`:

```ts
const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {
vpnGateway: true
});
```

VPN connections can then be added:
```ts
vpc.addVpnConnection('Dynamic', {
ip: '1.2.3.4'
});
```

Routes will be propagated on the route tables associated with the private subnets.
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 @@ -6,6 +6,7 @@ export * from './security-group-rule';
export * from './vpc';
export * from './vpc-ref';
export * from './vpc-network-provider';
export * from './vpn';

// AWS::EC2 CloudFormation Resources:
export * from './ec2.generated';
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk";
import { subnetName } from './util';
import { VpnConnection, VpnConnectionOptions } from './vpn';

export interface IVpcSubnet extends IConstruct {
/**
Expand Down Expand Up @@ -54,6 +55,11 @@ export interface IVpcNetwork extends IConstruct {
*/
readonly vpcRegion: string;

/**
* Identifier for the VPN gateway
*/
readonly vpnGatewayId?: string;

/**
* Return the subnets appropriate for the placement strategy
*/
Expand All @@ -68,6 +74,11 @@ export interface IVpcNetwork extends IConstruct {
*/
isPublicSubnet(subnet: IVpcSubnet): boolean;

/**
* Adds a new VPN connection to this VPC
*/
addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection;

/**
* Exports this VPC so it can be consumed by another stack.
*/
Expand Down Expand Up @@ -173,6 +184,11 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
*/
public abstract readonly availabilityZones: string[];

/**
* Identifier for the VPN gateway
*/
public abstract readonly vpnGatewayId?: string;

/**
* Dependencies for internet connectivity
*/
Expand Down Expand Up @@ -211,6 +227,16 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
}[placement.subnetsToUse];
}

/**
* Adds a new VPN connection to this VPC
*/
public addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection {
return new VpnConnection(this, id, {
vpc: this,
...options
});
}

/**
* Export this VPC from the stack
*/
Expand Down Expand Up @@ -291,6 +317,11 @@ export interface VpcNetworkImportProps {
* Must be undefined or have a name for every isolated subnet group.
*/
isolatedSubnetNames?: string[];

/**
* VPN gateway's identifier
*/
vpnGatewayId?: string;
}

export interface VpcSubnetImportProps {
Expand Down
68 changes: 66 additions & 2 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import cdk = require('@aws-cdk/cdk');
import { ConcreteDependable, IDependable } from '@aws-cdk/cdk';
import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute } from './ec2.generated';
import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnVPNGateway, CfnVPNGatewayRoutePropagation } from './ec2.generated';
import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment } from './ec2.generated';
import { NetworkBuilder } from './network-util';
import { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util';
import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider';
import { IVpcNetwork, IVpcSubnet, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcPlacementStrategy, VpcSubnetImportProps } from './vpc-ref';
import { VpnConnectionOptions, VpnConnectionType } from './vpn';

/**
* Name tag constant
Expand Down Expand Up @@ -115,6 +116,27 @@ export interface VpcNetworkProps {
* private subnet per AZ
*/
subnetConfiguration?: SubnetConfiguration[];

/**
* Indicates whether a VPN gateway should be created and attached to this VPC.
*
* @default true when vpnGatewayAsn or vpnConnections is specified.
*/
vpnGateway?: boolean;

/**
* The private Autonomous System Number (ASN) for the VPN gateway.
*
* @default Amazon default ASN
*/
vpnGatewayAsn?: number;

/**
* VPN connections to this VPC.
*
* @default no connections
*/
vpnConnections?: { [id: string]: VpnConnectionOptions }
}

/**
Expand Down Expand Up @@ -250,6 +272,11 @@ export class VpcNetwork extends VpcNetworkBase {
*/
public readonly availabilityZones: string[];

/**
* Identifier for the VPN gateway
*/
public readonly vpnGatewayId?: string;

/**
* The VPC resource
*/
Expand Down Expand Up @@ -343,6 +370,40 @@ export class VpcNetwork extends VpcNetworkBase {
privateSubnet.addDefaultNatRouteEntry(ngwId);
});
}

if ((props.vpnConnections || props.vpnGatewayAsn) && props.vpnGateway === false) {
throw new Error('Cannot specify `vpnConnections` or `vpnGatewayAsn` when `vpnGateway` is set to false.');
}

if (props.vpnGateway || props.vpnConnections || props.vpnGatewayAsn) {
const vpnGateway = new CfnVPNGateway(this, 'VpnGateway', {
amazonSideAsn: props.vpnGatewayAsn,
type: VpnConnectionType.IPsec1
});

const attachment = new CfnVPCGatewayAttachment(this, 'VPCVPNGW', {
vpcId: this.vpcId,
vpnGatewayId: vpnGateway.vpnGatewayName
});

this.vpnGatewayId = vpnGateway.vpnGatewayName;

// Propagate routes on route tables associated with private subnets
const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', {
routeTableIds: (this.privateSubnets as VpcPrivateSubnet[]).map(subnet => subnet.routeTableId),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider allowing an option to allow for subnetgroups to be chosen by the user of this to decide what to propogate - there are reasons you might want to route public (and isolated)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, on it.

vpnGatewayId: this.vpnGatewayId
});

// The AWS::EC2::VPNGatewayRoutePropagation resource cannot use the VPN gateway
// until it has successfully attached to the VPC.
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html
routePropagation.node.addDependency(attachment);

const vpnConnections = props.vpnConnections || {};
for (const [connectionId, connection] of Object.entries(vpnConnections)) {
this.addVpnConnection(connectionId, connection);
}
}
}

/**
Expand All @@ -355,6 +416,7 @@ export class VpcNetwork extends VpcNetworkBase {

return {
vpcId: new cdk.Output(this, 'VpcId', { value: this.vpcId }).makeImportValue().toString(),
vpnGatewayId: new cdk.Output(this, 'VpnGatewayId', { value: this.vpnGatewayId }).makeImportValue().toString(),
availabilityZones: this.availabilityZones,
publicSubnetIds: pub.ids,
publicSubnetNames: pub.names,
Expand Down Expand Up @@ -523,7 +585,7 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet {
/**
* The routeTableId attached to this subnet.
*/
private readonly routeTableId: string;
public readonly routeTableId: string;

private readonly internetDependencies = new ConcreteDependable();

Expand Down Expand Up @@ -653,12 +715,14 @@ class ImportedVpcNetwork extends VpcNetworkBase {
public readonly privateSubnets: IVpcSubnet[];
public readonly isolatedSubnets: IVpcSubnet[];
public readonly availabilityZones: string[];
public readonly vpnGatewayId?: string;

constructor(scope: cdk.Construct, id: string, private readonly props: VpcNetworkImportProps) {
super(scope, id);

this.vpcId = props.vpcId;
this.availabilityZones = props.availabilityZones;
this.vpnGatewayId = props.vpnGatewayId;

// tslint:disable:max-line-length
const pub = new ImportSubnetGroup(props.publicSubnetIds, props.publicSubnetNames, SubnetType.Public, this.availabilityZones, 'publicSubnetIds', 'publicSubnetNames');
Expand Down
135 changes: 135 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import cdk = require('@aws-cdk/cdk');
import { CfnCustomerGateway, CfnVPNConnection, CfnVPNConnectionRoute } from './ec2.generated';
import { IVpcNetwork } from './vpc-ref';

export interface IVpnConnection extends cdk.IConstruct {
/**
* The id of the VPN connection.
*/
readonly vpnId: string;

/**
* The id of the customer gateway.
*/
readonly customerGatewayId: string;

/**
* The ip address of the customer gateway.
*/
readonly customerGatewayIp: string;

/**
* The ASN of the customer gateway.
*/
readonly customerGatewayAsn: number;
}

export interface VpnTunnelOption {
eladb marked this conversation as resolved.
Show resolved Hide resolved
/**
* The pre-shared key (PSK) to establish initial authentication between the virtual
* private gateway and customer gateway.
*/
presharedKey: string;

/**
* The range of inside IP addresses for the tunnel. Any specified CIDR blocks must be
* unique across all VPN connections that use the same virtual private gateway.
*/
tunnelInsideCidr: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both things here are independently optional https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-vpnconnection-vpntunneloptionsspecification.html and so one might want to set one and not the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

}

export interface VpnConnectionOptions {
/**
* The ip address of the customer gateway.
*/
ip: string;

/**
* The ASN of the customer gateway.
*
* @default 65000
*/
asn?: number;

/**
* The static routes to be routed from the VPN gateway to the customer gateway.
*
* @default Dynamic routing (BGP)
*/
staticRoutes?: string[];

/**
* Tunnel options for the VPN connection.
*/
vpnTunnelOptions?: VpnTunnelOption[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests don't appear to cover this.

}

export interface VpnConnectionProps extends VpnConnectionOptions {
/**
* The VPC to connect to.
*/
vpc: IVpcNetwork;
}

/**
* The VPN connection type.
*/
export enum VpnConnectionType {
/**
* The IPsec 1 VPN connection type.
*/
IPsec1 = 'ipsec.1',

/**
* Dummy member
* TODO: remove once https://github.com/awslabs/jsii/issues/231 is fixed
*/
Dummy = 'ipsec.1'
}

export class VpnConnection extends cdk.Construct implements IVpnConnection {
public readonly vpnId: string;
public readonly customerGatewayId: string;
public readonly customerGatewayIp: string;
public readonly customerGatewayAsn: number;

constructor(scope: cdk.Construct, id: string, props: VpnConnectionProps) {
super(scope, id);

if (!props.vpc.vpnGatewayId) {
throw new Error('Cannot create a VPN connection when VPC has no VPN gateway.');
}

const type = VpnConnectionType.IPsec1;
const bgpAsn = props.asn || 65000;

const customerGateway = new CfnCustomerGateway(this, 'CustomerGateway', {
bgpAsn,
ipAddress: props.ip,
type
});

this.customerGatewayId = customerGateway.customerGatewayName;
this.customerGatewayAsn = bgpAsn;
this.customerGatewayIp = props.ip;

const vpnConnection = new CfnVPNConnection(this, 'Resource', {
type,
customerGatewayId: customerGateway.customerGatewayName,
staticRoutesOnly: props.staticRoutes ? true : false,
vpnGatewayId: props.vpc.vpnGatewayId,
vpnTunnelOptionsSpecifications: props.vpnTunnelOptions
});

this.vpnId = vpnConnection.vpnConnectionName;

if (props.staticRoutes) {
props.staticRoutes.forEach(route => {
new CfnVPNConnectionRoute(this, `Route${route.replace(/[^\d]/g, '')}`, {
destinationCidrBlock: route,
vpnConnectionId: this.vpnId
});
});
}
}
}
Loading