Skip to content

Commit

Permalink
fix(ec2): can't add VPN connections to a VPC progressively
Browse files Browse the repository at this point in the history
When creating a vpc that contains no subnets, you can not add a vpn gateway
or vpn connections to the vpc while creating. This is all good.
But I'm later adding subnets and want to add a vpn connection, then
it is not possible, the vpnGateay boolean in IVpc is a read only boolean
and is set to false and there's no way to change that. This commit adds
the method enableVpnGateway to later allow adding vpn connections when
startinh with a vpc with no subnets.
  • Loading branch information
moatazelmasry2 committed May 5, 2020
1 parent 19b19ee commit 9498e05
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 56 deletions.
105 changes: 68 additions & 37 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { ConcreteDependable, Construct, ContextProvider, DependableTrait, IConst
import * as cxapi from '@aws-cdk/cx-api';
import {
CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnRouteTable, CfnSubnet,
CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment, CfnVPNGateway, CfnVPNGatewayRoutePropagation } from './ec2.generated';
CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation } from './ec2.generated';
import { NatProvider } from './nat';
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
import { NetworkBuilder } from './network-util';
import { allRouteTableIds, defaultSubnetName, ImportSubnetGroup, subnetGroupNameFromConstructId, subnetId } from './util';
import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions, InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint';
import { FlowLog, FlowLogOptions, FlowLogResourceType } from './vpc-flow-logs';
import { VpcLookupOptions } from './vpc-lookup';
import { VpnConnection, VpnConnectionOptions, VpnConnectionType } from './vpn';
import { EnableVpnGatewayOptions, VpnConnection, VpnConnectionOptions, VpnConnectionType, VpnGateway } from './vpn';

const VPC_SUBNET_SYMBOL = Symbol.for('@aws-cdk/aws-ec2.VpcSubnet');

Expand Down Expand Up @@ -106,6 +106,11 @@ export interface IVpc extends IResource {
*/
selectSubnets(selection?: SubnetSelection): SelectedSubnets;

/**
* Adds a VPN Gateway to this VPC
*/
enableVpnGateway(options: EnableVpnGatewayOptions): void;

/**
* Adds a new VPN connection to this VPC
*/
Expand Down Expand Up @@ -305,11 +310,6 @@ abstract class VpcBase extends Resource implements IVpc {
*/
public abstract readonly availabilityZones: string[];

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

/**
* Dependencies for internet connectivity
*/
Expand All @@ -327,6 +327,13 @@ abstract class VpcBase extends Resource implements IVpc {
*/
protected incompleteSubnetDefinition: boolean = false;

/**
* Mutable private field for the vpnGatewayId
*
* @internal
*/
protected _vpnGatewayId?: string;

/**
* Returns IDs of selected subnets
*/
Expand All @@ -343,6 +350,44 @@ abstract class VpcBase extends Resource implements IVpc {
};
}

/**
* Adds a VPN Gateway to this VPC
*/
public enableVpnGateway(options: EnableVpnGatewayOptions): void {
if (this.vpnGatewayId) {
throw new Error('The VPN Gateway has already been enabled.');
}

const vpnGateway = new VpnGateway(this, 'VpnGateway', {
amazonSideAsn: options.amazonSideAsn,
type: VpnConnectionType.IPSEC_1,
});

this._vpnGatewayId = vpnGateway.gatewayId;

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

// Propagate routes on route tables associated with the right subnets
const vpnRoutePropagation = options.vpnRoutePropagation ?? [{}];
const routeTableIds = allRouteTableIds(...vpnRoutePropagation.map(s => this.selectSubnets(s)));

if (routeTableIds.length === 0) {
this.node.addError(`enableVpnGateway: no subnets matching selection: '${JSON.stringify(vpnRoutePropagation)}'. Select other subnets to add routes to.`);
}

const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', {
routeTableIds,
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);
}

/**
* Adds a new VPN connection to this VPC
*/
Expand Down Expand Up @@ -383,6 +428,13 @@ abstract class VpcBase extends Resource implements IVpc {
});
}

/**
* Returns the id of the VPN Gateway (if enabled)
*/
public get vpnGatewayId(): string | undefined {
return this._vpnGatewayId;
}

/**
* Return the subnets appropriate for the placement strategy
*/
Expand Down Expand Up @@ -1029,11 +1081,6 @@ export class Vpc extends VpcBase {
*/
public readonly availabilityZones: string[];

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

public readonly internetConnectivityEstablished: IDependable;

/**
Expand Down Expand Up @@ -1136,36 +1183,21 @@ export class Vpc extends VpcBase {
}
}

if (props.vpnGateway && this.publicSubnets.length === 0 && this.privateSubnets.length === 0 && this.isolatedSubnets.length === 0) {
throw new Error('Can not enable the VPN gateway while the VPC has no subnets at all');
}

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', {
this.enableVpnGateway({
amazonSideAsn: props.vpnGatewayAsn,
type: VpnConnectionType.IPSEC_1,
vpnRoutePropagation: props.vpnRoutePropagation,
});

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

this.vpnGatewayId = vpnGateway.ref;

// Propagate routes on route tables associated with the right subnets
const vpnRoutePropagation = props.vpnRoutePropagation ?? [{}];
const routeTableIds = allRouteTableIds(...vpnRoutePropagation.map(s => this.selectSubnets(s)));
const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', {
routeTableIds,
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 Down Expand Up @@ -1684,7 +1716,6 @@ class ImportedVpc extends VpcBase {
public readonly privateSubnets: ISubnet[];
public readonly isolatedSubnets: ISubnet[];
public readonly availabilityZones: string[];
public readonly vpnGatewayId?: string;
public readonly internetConnectivityEstablished: IDependable = new ConcreteDependable();
private readonly cidr?: string | undefined;

Expand All @@ -1694,7 +1725,7 @@ class ImportedVpc extends VpcBase {
this.vpcId = props.vpcId;
this.cidr = props.vpcCidrBlock;
this.availabilityZones = props.availabilityZones;
this.vpnGatewayId = props.vpnGatewayId;
this._vpnGatewayId = props.vpnGatewayId;
this.incompleteSubnetDefinition = isIncomplete;

// tslint:disable:max-line-length
Expand All @@ -1718,7 +1749,7 @@ class ImportedVpc extends VpcBase {

class LookedUpVpc extends VpcBase {
public readonly vpcId: string;
public readonly vpnGatewayId?: string;
public readonly vpnGatewayId: string | undefined;
public readonly internetConnectivityEstablished: IDependable = new ConcreteDependable();
public readonly availabilityZones: string[];
public readonly publicSubnets: ISubnet[];
Expand All @@ -1731,7 +1762,7 @@ class LookedUpVpc extends VpcBase {

this.vpcId = props.vpcId;
this.cidr = props.vpcCidrBlock;
this.vpnGatewayId = props.vpnGatewayId;
this._vpnGatewayId = props.vpnGatewayId;
this.incompleteSubnetDefinition = isIncomplete;

const subnetGroups = props.subnetGroups || [];
Expand Down
75 changes: 72 additions & 3 deletions packages/@aws-cdk/aws-ec2/lib/vpn.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as cdk from '@aws-cdk/core';
import * as net from 'net';
import { CfnCustomerGateway, CfnVPNConnection, CfnVPNConnectionRoute } from './ec2.generated';
import { IVpc } from './vpc';
import {
CfnCustomerGateway,
CfnVPNConnection,
CfnVPNConnectionRoute,
CfnVPNGateway,
} from './ec2.generated';
import {IVpc, SubnetSelection} from './vpc';

export interface IVpnConnection extends cdk.IResource {
/**
Expand All @@ -26,6 +31,17 @@ export interface IVpnConnection extends cdk.IResource {
readonly customerGatewayAsn: number;
}

/**
* The virtual private gateway interface
*/
export interface IVpnGateway extends cdk.IResource {

/**
* The virtual private gateway Id
*/
readonly gatewayId: string
}

export interface VpnTunnelOption {
/**
* The pre-shared key (PSK) to establish initial authentication between the virtual
Expand Down Expand Up @@ -75,6 +91,34 @@ export interface VpnConnectionOptions {
readonly tunnelOptions?: VpnTunnelOption[];
}

/**
* The VpnGateway Properties
*/
export interface VpnGatewayProps {

/**
* Default type ipsec.1
*/
readonly type: string;

/**
* Explicitely specify an Asn or let aws pick an Asn for you.
* @default 65000
*/
readonly amazonSideAsn?: number;
}

/**
* Options for the Vpc.enableVpnGateway() method
*/
export interface EnableVpnGatewayOptions extends VpnGatewayProps {
/**
* Provide an array of subnets where the route propagation shoud be added.
* @default noPropagation
*/
readonly vpnRoutePropagation?: SubnetSelection[]
}

export interface VpnConnectionProps extends VpnConnectionOptions {
/**
* The VPC to connect to.
Expand All @@ -98,6 +142,28 @@ export enum VpnConnectionType {
DUMMY = 'dummy'
}

/**
* The VPN Gateway that shall be added to the VPC
*
* @resource AWS::EC2::VPNGateway
*/
export class VpnGateway extends cdk.Resource implements IVpnGateway {

/**
* The virtual private gateway Id
*/
public readonly gatewayId: string;

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

// This is 'Default' instead of 'Resource', because using 'Default' will generate
// a logical ID for a VpnGateway which is exactly the same as the logical ID that used
// to be created for the CfnVPNGateway (and 'Resource' would not do that).
const vpnGW = new CfnVPNGateway(this, 'Default', props);
this.gatewayId = vpnGW.ref;
}
}
/**
* Define a VPN Connection
*
Expand Down Expand Up @@ -151,7 +217,10 @@ export class VpnConnection extends cdk.Resource implements IVpnConnection {
super(scope, id);

if (!props.vpc.vpnGatewayId) {
throw new Error('Cannot create a VPN connection when VPC has no VPN gateway.');
props.vpc.enableVpnGateway({
type: 'ipsec.1',
amazonSideAsn: props.asn,
});
}

if (!net.isIPv4(props.ip)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-ec2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,8 @@
"docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_SQL_2008_R2_SP3_WEB",
"docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_PORTUGESE_BRAZIL_64BIT_BASE",
"props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroupId",
"props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroups"
"props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroups",
"props-physical-name:@aws-cdk/aws-ec2.VpnGatewayProps"
]
},
"stability": "stable",
Expand Down
53 changes: 38 additions & 15 deletions packages/@aws-cdk/aws-ec2/test/test.vpn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, haveResource } from '@aws-cdk/assert';
import { Duration, Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { Vpc, VpnConnection } from '../lib';
import { PublicSubnet, Vpc, VpnConnection } from '../lib';

export = {
'can add a vpn connection to a vpc with a vpn gateway'(test: Test) {
Expand Down Expand Up @@ -123,20 +123,6 @@ export = {
test.done();
},

'fails when vpc has no vpn gateway'(test: Test) {
// GIVEN
const stack = new Stack();

const vpc = new Vpc(stack, 'VpcNetwork');

test.throws(() => vpc.addVpnConnection('VpnConnection', {
asn: 65000,
ip: '192.0.2.1',
}), /VPN gateway/);

test.done();
},

'fails when ip is invalid'(test: Test) {
// GIVEN
const stack = new Stack();
Expand Down Expand Up @@ -299,4 +285,41 @@ export = {

test.done();
},

'fails when enabling vpnGateway without having subnets'(test: Test) {
// GIVEN
const stack = new Stack();

test.throws(() => new Vpc(stack, 'VpcNetwork', {
vpnGateway: true,
subnetConfiguration: [],
}), /VPN gateway/);

test.done();
},

'can add a vpn connection later to a vpc that initially had no subnets'(test: Test) {
// GIVEN
const stack = new Stack();

// WHEN
const vpc = new Vpc(stack, 'VpcNetwork', {
subnetConfiguration: [],
});
const subnet = new PublicSubnet(stack, 'Subnet', {
vpcId: vpc.vpcId,
availabilityZone: 'eu-central-1a',
cidrBlock: '10.0.0.0/28',
});
vpc.publicSubnets.push(subnet);
vpc.addVpnConnection('VPNConnection', {
ip: '1.2.3.4',
});

// THEN
expect(stack).to(haveResource('AWS::EC2::CustomerGateway', {
Type: 'ipsec.1',
}));
test.done();
},
};

0 comments on commit 9498e05

Please sign in to comment.