From 45e69c77696ae42c6c003312d070cf70b8b672ab Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 Feb 2019 09:56:53 +0100 Subject: [PATCH 01/14] feat(ec2): add support for vpn connections --- packages/@aws-cdk/aws-ec2/README.md | 30 + packages/@aws-cdk/aws-ec2/lib/index.ts | 1 + packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 37 ++ packages/@aws-cdk/aws-ec2/lib/vpc.ts | 54 +- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 123 ++++ .../aws-ec2/test/integ.vpn.expected.json | 629 ++++++++++++++++++ packages/@aws-cdk/aws-ec2/test/integ.vpn.ts | 26 + packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 40 ++ packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 101 +++ packages/@aws-cdk/cx-api/lib/context/vpc.ts | 7 +- .../aws-cdk/lib/context-providers/vpcs.ts | 24 +- 11 files changed, 1067 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/lib/vpn.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.vpn.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/test.vpn.ts diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 5258f2ca7414c..fe62a1c75505c 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -303,3 +303,33 @@ 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 a VPN gateway: + +```ts +const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { + vpnGateway: true +}); +``` + +Then, add connections: + +```ts +// Dynamic routing +vpc.newVpnConnection('Dynamic', { + ip: '1.2.3.4' +}); + +// Static routing +vpc.newVpnConnection('Static', { + ip: '4.5.6.7', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] +}); +``` + +Routes will be propagated on the route tables associated with the private subnets. diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index bade088217cae..1188dff779727 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -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'; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index 0bab75866938e..b3e8d0f5b8599 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,5 +1,6 @@ import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk"; import { subnetName } from './util'; +import { BaseVpnConnectionProps, VpnConnection } from './vpn'; export interface IVpcSubnet extends IConstruct { /** @@ -12,6 +13,12 @@ export interface IVpcSubnet extends IConstruct { */ readonly subnetId: string; + /** + * The id of the route table associated with this subnet. + * Not available for an imported subnet. + */ + readonly routeTableId: string; + /** * Dependable that can be depended upon to force internet connectivity established on the VPC */ @@ -54,6 +61,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 */ @@ -68,6 +80,11 @@ export interface IVpcNetwork extends IConstruct { */ isPublicSubnet(subnet: IVpcSubnet): boolean; + /** + * Adds a new VPN connection to this VPC + */ + newVpnConnection(id: string, props: BaseVpnConnectionProps): VpnConnection; + /** * Exports this VPC so it can be consumed by another stack. */ @@ -173,6 +190,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 */ @@ -211,6 +233,16 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { }[placement.subnetsToUse]; } + /** + * Adds a new VPN connection to this VPC + */ + public newVpnConnection(id: string, props: BaseVpnConnectionProps): VpnConnection { + return new VpnConnection(this, id, { + vpc: this, + ...props + }); + } + /** * Export this VPC from the stack */ @@ -291,6 +323,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 { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 792093a64ba4c..ecd73c77ea4c3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -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 { IPsec1 } from './vpn'; /** * Name tag constant @@ -115,6 +116,20 @@ export interface VpcNetworkProps { * private subnet per AZ */ subnetConfiguration?: SubnetConfiguration[]; + + /** + * Indicates whether a VPN gateway should be created and attached to this VPC. + * + * @default false + */ + vpnGateway?: boolean; + + /** + * The private Autonomous System Number (ASN) for the VPN gateway. + * + * @default Amazon default ASN + */ + vpnGatewayAsn?: number; } /** @@ -250,6 +265,11 @@ export class VpcNetwork extends VpcNetworkBase { */ public readonly availabilityZones: string[]; + /** + * Identifier for the VPN gateway + */ + public readonly vpnGatewayId?: string; + /** * The VPC resource */ @@ -343,6 +363,31 @@ export class VpcNetwork extends VpcNetworkBase { privateSubnet.addDefaultNatRouteEntry(ngwId); }); } + + if (props.vpnGateway) { + const vpnGateway = new CfnVPNGateway(this, 'VpnGateway', { + amazonSideAsn: props.vpnGatewayAsn, + type: 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.map(subnet => subnet.routeTableId!), + 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); + } } /** @@ -355,6 +400,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, @@ -523,7 +569,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(); @@ -653,12 +699,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'); @@ -680,12 +728,14 @@ class ImportedVpcSubnet extends cdk.Construct implements IVpcSubnet { public readonly internetConnectivityEstablished: cdk.IDependable = new cdk.ConcreteDependable(); public readonly availabilityZone: string; public readonly subnetId: string; + public readonly routeTableId: string; constructor(scope: cdk.Construct, id: string, private readonly props: VpcSubnetImportProps) { super(scope, id); this.subnetId = props.subnetId; this.availabilityZone = props.availabilityZone; + this.routeTableId = ''; } public export() { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts new file mode 100644 index 0000000000000..1f06a6682ecc5 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -0,0 +1,123 @@ +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 { + /** + * 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; +} + +export interface BaseVpnConnectionProps { + /** + * 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[]; +} + +export interface VpnConnectionProps extends BaseVpnConnectionProps { + /** + * The VPC to connect to. + */ + vpc: IVpcNetwork; +} + +/** + * The IPsec 1 VPN connection type. + */ +export const IPsec1 = '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 = 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 + }); + }); + } + } +} diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json new file mode 100644 index 0000000000000..551e467ea2e1f --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json @@ -0,0 +1,629 @@ +{ + "Resources": { + "MyVpcF9F0CA6F": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.10.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcPublicSubnet1SubnetF6608456": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.0.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet1RouteTableC46AB2F4": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet1" + } + ] + } + }, + "MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + } + } + }, + "MyVpcPublicSubnet1DefaultRoute95FDF9EB": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet1EIP096967CB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet1NATGatewayAD3400C1": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet1EIP096967CB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet1" + } + ] + } + }, + "MyVpcPublicSubnet2Subnet492B6BFB": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.32.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet2RouteTable1DF17386": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet2" + } + ] + } + }, + "MyVpcPublicSubnet2RouteTableAssociation227DE78D": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + } + } + }, + "MyVpcPublicSubnet2DefaultRoute052936F6": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet2EIP8CCBA239": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet2NATGateway91BFBEC9": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet2EIP8CCBA239", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet2" + } + ] + } + }, + "MyVpcPublicSubnet3Subnet57EEE236": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.64.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVpcPublicSubnet3RouteTable15028F08": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet3" + } + ] + } + }, + "MyVpcPublicSubnet3RouteTableAssociation5C27DDA4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + } + } + }, + "MyVpcPublicSubnet3DefaultRoute3A83AB36": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + }, + "DependsOn": [ + "MyVpcVPCGW488ACE0D" + ] + }, + "MyVpcPublicSubnet3EIPC5ACADAB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet3NATGatewayD4B50EBE": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet3EIPC5ACADAB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet3" + } + ] + } + }, + "MyVpcPrivateSubnet1Subnet5057CF7E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.96.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet1RouteTable8819E6E2": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet1" + } + ] + } + }, + "MyVpcPrivateSubnet1RouteTableAssociation56D38C7E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet1Subnet5057CF7E" + } + } + }, + "MyVpcPrivateSubnet1DefaultRouteA8CDE2FA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet1NATGatewayAD3400C1" + } + } + }, + "MyVpcPrivateSubnet2Subnet0040C983": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.128.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet2RouteTableCEDCEECE": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet2" + } + ] + } + }, + "MyVpcPrivateSubnet2RouteTableAssociation86A610DA": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet2Subnet0040C983" + } + } + }, + "MyVpcPrivateSubnet2DefaultRoute9CE96294": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet2NATGateway91BFBEC9" + } + } + }, + "MyVpcPrivateSubnet3Subnet772D6AD7": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.10.160.0/19", + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVpcPrivateSubnet3RouteTableB790927C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet3" + } + ] + } + }, + "MyVpcPrivateSubnet3RouteTableAssociationD951741C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet3Subnet772D6AD7" + } + } + }, + "MyVpcPrivateSubnet3DefaultRouteEC11C0C5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet3NATGatewayD4B50EBE" + } + } + }, + "MyVpcIGW5C4A4F63": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcVPCGW488ACE0D": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "InternetGatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + } + } + }, + "MyVpcVpnGateway11FB05E5": { + "Type": "AWS::EC2::VPNGateway", + "Properties": { + "Type": "ipsec.1", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcVPCVPNGW0CB969B3": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + }, + "VpnGatewayId": { + "Ref": "MyVpcVpnGateway11FB05E5" + } + } + }, + "MyVpcRoutePropagation122FC3BE": { + "Type": "AWS::EC2::VPNGatewayRoutePropagation", + "Properties": { + "RouteTableIds": [ + { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + } + ], + "VpnGatewayId": { + "Ref": "MyVpcVpnGateway11FB05E5" + } + }, + "DependsOn": [ + "MyVpcVPCVPNGW0CB969B3" + ] + }, + "MyVpcDynamicCustomerGatewayFB63DFBF": { + "Type": "AWS::EC2::CustomerGateway", + "Properties": { + "BgpAsn": 65000, + "IpAddress": "52.85.255.164", + "Type": "ipsec.1", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcDynamic739F3519": { + "Type": "AWS::EC2::VPNConnection", + "Properties": { + "CustomerGatewayId": { + "Ref": "MyVpcDynamicCustomerGatewayFB63DFBF" + }, + "Type": "ipsec.1", + "VpnGatewayId": { + "Ref": "MyVpcVpnGateway11FB05E5" + }, + "StaticRoutesOnly": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcStaticCustomerGateway43D01906": { + "Type": "AWS::EC2::CustomerGateway", + "Properties": { + "BgpAsn": 65000, + "IpAddress": "52.85.255.197", + "Type": "ipsec.1", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcStaticABA7F625": { + "Type": "AWS::EC2::VPNConnection", + "Properties": { + "CustomerGatewayId": { + "Ref": "MyVpcStaticCustomerGateway43D01906" + }, + "Type": "ipsec.1", + "VpnGatewayId": { + "Ref": "MyVpcVpnGateway11FB05E5" + }, + "StaticRoutesOnly": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc" + } + ] + } + }, + "MyVpcStaticRoute192168100240A24A5CC": { + "Type": "AWS::EC2::VPNConnectionRoute", + "Properties": { + "DestinationCidrBlock": "192.168.10.0/24", + "VpnConnectionId": { + "Ref": "MyVpcStaticABA7F625" + } + } + }, + "MyVpcStaticRoute19216820024CD4B642F": { + "Type": "AWS::EC2::VPNConnectionRoute", + "Properties": { + "DestinationCidrBlock": "192.168.20.0/24", + "VpnConnectionId": { + "Ref": "MyVpcStaticABA7F625" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts new file mode 100644 index 0000000000000..6f0a258db0d29 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts @@ -0,0 +1,26 @@ +import cdk = require('@aws-cdk/cdk'); +import ec2 = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-ec2-vpn'); + +const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { + cidr: '10.10.0.0/16', + vpnGateway: true +}); + +// Dynamic routing +vpc.newVpnConnection('Dynamic', { + ip: '52.85.255.164' +}); + +// Static routing +vpc.newVpnConnection('Static', { + ip: '52.85.255.197', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 60c6a28b38276..9af1d70638733 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -323,6 +323,46 @@ export = { })); test.done(); }, + 'with a vpn gateway'(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'VPC', { + vpnGateway: true, + vpnGatewayAsn: 65000 + }); + + expect(stack).to(haveResource('AWS::EC2::VPNGateway', { + AmazonSideAsn: 65000, + Type: 'ipsec.1' + })); + + expect(stack).to(haveResource('AWS::EC2::VPCGatewayAttachment', { + VpcId: { + Ref: 'VPCB9E5F0B4' + }, + VpnGatewayId: { + Ref: 'VPCVpnGatewayB5ABAE68' + } + })); + + expect(stack).to(haveResource('AWS::EC2::VPNGatewayRoutePropagation', { + RouteTableIds: [ + { + Ref: 'VPCPrivateSubnet1RouteTableBE8A6027' + }, + { + Ref: 'VPCPrivateSubnet2RouteTable0A19E10E' + }, + { + Ref: 'VPCPrivateSubnet3RouteTable192186F8' + } + ], + VpnGatewayId: { + Ref: 'VPCVpnGatewayB5ABAE68' + } + })); + + test.done(); + } }, diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts new file mode 100644 index 0000000000000..cedf0a53c54f6 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -0,0 +1,101 @@ +import { expect, haveResource, } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { VpcNetwork } from '../lib'; + +export = { + 'can add a vpn connection to a vpc with a vpn gateway'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const vpc = new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: true, + }); + + vpc.newVpnConnection('VpnConnection', { + asn: 65001, + ip: '192.0.2.1', + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::CustomerGateway', { + BgpAsn: 65001, + IpAddress: '192.0.2.1', + Type: 'ipsec.1' + })); + + expect(stack).to(haveResource('AWS::EC2::VPNConnection', { + CustomerGatewayId: { + Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF' + }, + Type: 'ipsec.1', + VpnGatewayId: { + Ref: 'VpcNetworkVpnGateway501295FA' + }, + StaticRoutesOnly: false, + })); + + test.done(); + }, + + 'with static routing'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const vpc = new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: true, + }); + + vpc.newVpnConnection('VpnConnection', { + ip: '192.0.2.1', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPNConnection', { + CustomerGatewayId: { + Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF' + }, + Type: 'ipsec.1', + VpnGatewayId: { + Ref: 'VpcNetworkVpnGateway501295FA' + }, + StaticRoutesOnly: true, + })); + + expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { + DestinationCidrBlock: '192.168.10.0/24', + VpnConnectionId: { + Ref: 'VpcNetworkVpnConnectionFB5C15BC' + } + })); + + expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { + DestinationCidrBlock: '192.168.20.0/24', + VpnConnectionId: { + Ref: 'VpcNetworkVpnConnectionFB5C15BC' + } + })); + + test.done(); + }, + + 'fails when vpc has no vpn gateway'(test: Test) { + // GIVEN + const stack = new Stack(); + + const vpc = new VpcNetwork(stack, 'VpcNetwork'); + + test.throws(() => vpc.newVpnConnection('VpnConnection', { + asn: 65000, + ip: '192.0.2.1' + }), /VPN gateway/); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/cx-api/lib/context/vpc.ts b/packages/@aws-cdk/cx-api/lib/context/vpc.ts index a4513b2567a99..8a4490cfa3fe8 100644 --- a/packages/@aws-cdk/cx-api/lib/context/vpc.ts +++ b/packages/@aws-cdk/cx-api/lib/context/vpc.ts @@ -80,4 +80,9 @@ export interface VpcContextResponse { * Element count: #(isolatedGroups) */ isolatedSubnetNames?: string[]; -} \ No newline at end of file + + /** + * The VPN gateway ID + */ + vpnGatewayId?: string; +} diff --git a/packages/aws-cdk/lib/context-providers/vpcs.ts b/packages/aws-cdk/lib/context-providers/vpcs.ts index 442b55d577a7e..dd4cd7bdda45d 100644 --- a/packages/aws-cdk/lib/context-providers/vpcs.ts +++ b/packages/aws-cdk/lib/context-providers/vpcs.ts @@ -41,8 +41,8 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin { private async readVpcProps(ec2: AWS.EC2, vpcId: string): Promise { debug(`Describing VPC ${vpcId}`); - const response = await ec2.describeSubnets({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }).promise(); - const listedSubnets = response.Subnets || []; + const subnetsResponse = await ec2.describeSubnets({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }).promise(); + const listedSubnets = subnetsResponse.Subnets || []; // Now comes our job to separate these subnets out into AZs and subnet groups (Public, Private, Isolated) // We have the following attributes to go on: @@ -68,6 +68,25 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin { const grouped = groupSubnets(subnets); + // Find attached+available VPN gateway for this VPC + const vpnGatewayResponse = await ec2.describeVpnGateways({ + Filters: [ + { + Name: 'attachment.vpc-id', + Values: [vpcId] + }, + { + Name: 'attachment.state', + Values: ['attached'] + }, + { + Name: 'state', + Values: ['available'] + } + ] + }).promise(); + const vpnGatewayId = vpnGatewayResponse.VpnGateways ? vpnGatewayResponse.VpnGateways[0].VpnGatewayId : undefined; + return { vpcId, availabilityZones: grouped.azs, @@ -77,6 +96,7 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin { privateSubnetNames: collapse(flatMap(findGroups(SubnetType.Private, grouped), group => group.name ? [group.name] : [])), publicSubnetIds: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.subnets.map(s => s.subnetId))), publicSubnetNames: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.name ? [group.name] : [])), + vpnGatewayId, }; } } From 5312496ca8340174faa5cc2f1fe883c52f92a42f Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 19:08:09 +0100 Subject: [PATCH 02/14] Address PR comments --- packages/@aws-cdk/aws-ec2/README.md | 28 ++++++++-- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 8 +-- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 26 +++++++-- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 15 ++++-- packages/@aws-cdk/aws-ec2/test/integ.vpn.ts | 29 +++++----- packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 59 +++++++++++++-------- 6 files changed, 109 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index fe62a1c75505c..dd0bdb81d7d01 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -306,24 +306,42 @@ selectable by instantiating one of these classes: ### VPN connections to a VPC -Create your VPC with a VPN gateway: +Create your VPC with VPN connections by specifying the `vpnConnections` props (keys are construct `id`s): ```ts const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { - vpnGateway: true + 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' + ] + } + } }); ``` -Then, add connections: +To export 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 // Dynamic routing -vpc.newVpnConnection('Dynamic', { +vpc.addVpnConnection('Dynamic', { ip: '1.2.3.4' }); // Static routing -vpc.newVpnConnection('Static', { +vpc.addVpnConnection('Static', { ip: '4.5.6.7', staticRoutes: [ '192.168.10.0/24', diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index b3e8d0f5b8599..08dc2f78b3151 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,6 +1,6 @@ import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk"; import { subnetName } from './util'; -import { BaseVpnConnectionProps, VpnConnection } from './vpn'; +import { VpnConnection, VpnConnectionOptions } from './vpn'; export interface IVpcSubnet extends IConstruct { /** @@ -83,7 +83,7 @@ export interface IVpcNetwork extends IConstruct { /** * Adds a new VPN connection to this VPC */ - newVpnConnection(id: string, props: BaseVpnConnectionProps): VpnConnection; + addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection; /** * Exports this VPC so it can be consumed by another stack. @@ -236,10 +236,10 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { /** * Adds a new VPN connection to this VPC */ - public newVpnConnection(id: string, props: BaseVpnConnectionProps): VpnConnection { + public addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection { return new VpnConnection(this, id, { vpc: this, - ...props + ...options }); } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index ecd73c77ea4c3..a0f320cce602d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -6,7 +6,7 @@ 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 { IPsec1 } from './vpn'; +import { VpnConnectionOptions, VpnConnectionType } from './vpn'; /** * Name tag constant @@ -120,7 +120,7 @@ export interface VpcNetworkProps { /** * Indicates whether a VPN gateway should be created and attached to this VPC. * - * @default false + * @default true when vpnConnections is specified, false otherwise. */ vpnGateway?: boolean; @@ -130,6 +130,13 @@ export interface VpcNetworkProps { * @default Amazon default ASN */ vpnGatewayAsn?: number; + + /** + * VPN connections to this VPC. + * + * @default no connections + */ + vpnConnections?: { [id: string]: VpnConnectionOptions } } /** @@ -364,10 +371,14 @@ export class VpcNetwork extends VpcNetworkBase { }); } - if (props.vpnGateway) { + if (props.vpnConnections && props.vpnGateway === false) { + throw new Error('Cannot specify `vpnConnections` when `vpnGateway` is set to false.'); + } + + if (props.vpnGateway || props.vpnConnections) { const vpnGateway = new CfnVPNGateway(this, 'VpnGateway', { amazonSideAsn: props.vpnGatewayAsn, - type: IPsec1 + type: VpnConnectionType.IPsec1 }); const attachment = new CfnVPCGatewayAttachment(this, 'VPCVPNGW', { @@ -379,7 +390,7 @@ export class VpcNetwork extends VpcNetworkBase { // Propagate routes on route tables associated with private subnets const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { - routeTableIds: this.privateSubnets.map(subnet => subnet.routeTableId!), + routeTableIds: this.privateSubnets.map(subnet => subnet.routeTableId), vpnGatewayId: this.vpnGatewayId }); @@ -387,6 +398,11 @@ export class VpcNetwork extends VpcNetworkBase { // 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 || {}; + Object.keys(vpnConnections).forEach(cId => { + this.addVpnConnection(cId, vpnConnections[cId]); + }); } } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 1f06a6682ecc5..963fa78c16de3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -37,7 +37,7 @@ export interface VpnTunnelOption { tunnelInsideCidr: string; } -export interface BaseVpnConnectionProps { +export interface VpnConnectionOptions { /** * The ip address of the customer gateway. */ @@ -63,7 +63,7 @@ export interface BaseVpnConnectionProps { vpnTunnelOptions?: VpnTunnelOption[]; } -export interface VpnConnectionProps extends BaseVpnConnectionProps { +export interface VpnConnectionProps extends VpnConnectionOptions { /** * The VPC to connect to. */ @@ -71,9 +71,14 @@ export interface VpnConnectionProps extends BaseVpnConnectionProps { } /** - * The IPsec 1 VPN connection type. + * The VPN connection type. */ -export const IPsec1 = 'ipsec.1'; +export enum VpnConnectionType { + /** + * The IPsec 1 VPN connection type. + */ + IPsec1 = 'ipsec.1' +} export class VpnConnection extends cdk.Construct implements IVpnConnection { public readonly vpnId: string; @@ -88,7 +93,7 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { throw new Error('Cannot create a VPN connection when VPC has no VPN gateway.'); } - const type = IPsec1; + const type = VpnConnectionType.IPsec1; const bgpAsn = props.asn || 65000; const customerGateway = new CfnCustomerGateway(this, 'CustomerGateway', { diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts index 6f0a258db0d29..b6a6fe60f4b53 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts @@ -4,23 +4,20 @@ import ec2 = require('../lib'); const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-ec2-vpn'); -const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { +new ec2.VpcNetwork(stack, 'MyVpc', { cidr: '10.10.0.0/16', - vpnGateway: true -}); - -// Dynamic routing -vpc.newVpnConnection('Dynamic', { - ip: '52.85.255.164' -}); - -// Static routing -vpc.newVpnConnection('Static', { - ip: '52.85.255.197', - staticRoutes: [ - '192.168.10.0/24', - '192.168.20.0/24' - ] + vpnConnections: { + Dynamic: { // Dynamic routing + ip: '52.85.255.164' + }, + Static: { // Static routing + ip: '52.85.255.197', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] + } + } }); app.run(); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts index cedf0a53c54f6..1940b51b48ce2 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -9,13 +9,13 @@ export = { const stack = new Stack(); // WHEN - const vpc = new VpcNetwork(stack, 'VpcNetwork', { - vpnGateway: true, - }); - - vpc.newVpnConnection('VpnConnection', { - asn: 65001, - ip: '192.0.2.1', + new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + asn: 65001, + ip: '192.0.2.1' + } + } }); // THEN @@ -44,22 +44,22 @@ export = { const stack = new Stack(); // WHEN - const vpc = new VpcNetwork(stack, 'VpcNetwork', { - vpnGateway: true, - }); - - vpc.newVpnConnection('VpnConnection', { - ip: '192.0.2.1', - staticRoutes: [ - '192.168.10.0/24', - '192.168.20.0/24' - ] + new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + static: { + ip: '192.0.2.1', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] + } + } }); // THEN expect(stack).to(haveResource('AWS::EC2::VPNConnection', { CustomerGatewayId: { - Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF' + Ref: 'VpcNetworkstaticCustomerGatewayAF2651CC' }, Type: 'ipsec.1', VpnGatewayId: { @@ -71,14 +71,14 @@ export = { expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { DestinationCidrBlock: '192.168.10.0/24', VpnConnectionId: { - Ref: 'VpcNetworkVpnConnectionFB5C15BC' + Ref: 'VpcNetworkstaticE33EA98C' } })); expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { DestinationCidrBlock: '192.168.20.0/24', VpnConnectionId: { - Ref: 'VpcNetworkVpnConnectionFB5C15BC' + Ref: 'VpcNetworkstaticE33EA98C' } })); @@ -91,11 +91,28 @@ export = { const vpc = new VpcNetwork(stack, 'VpcNetwork'); - test.throws(() => vpc.newVpnConnection('VpnConnection', { + test.throws(() => vpc.addVpnConnection('VpnConnection', { asn: 65000, ip: '192.0.2.1' }), /VPN gateway/); + test.done(); + }, + + 'fails when specifying vpnConnections with vpnGateway set to false'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: false, + vpnConnections: { + VpnConnection: { + asn: 65000, + ip: '192.0.2.1' + } + } + }), /`vpnConnections`.+`vpnGateway`.+false/); + test.done(); } }; From e95b7ffb0a31d6b4e925235201ebdf37c9b2692c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 19:10:57 +0100 Subject: [PATCH 03/14] Update README --- packages/@aws-cdk/aws-ec2/README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index dd0bdb81d7d01..59cbbfd91b417 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -335,19 +335,9 @@ const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { VPN connections can then be added: ```ts -// Dynamic routing vpc.addVpnConnection('Dynamic', { ip: '1.2.3.4' }); - -// Static routing -vpc.addVpnConnection('Static', { - ip: '4.5.6.7', - staticRoutes: [ - '192.168.10.0/24', - '192.168.20.0/24' - ] -}); ``` Routes will be propagated on the route tables associated with the private subnets. From 766c882d3d726ac22f9a8ab035d2f7dc7407cec4 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 19:16:14 +0100 Subject: [PATCH 04/14] Remove routeTableId from IVpcSubnet --- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 6 ------ packages/@aws-cdk/aws-ec2/lib/vpc.ts | 4 +--- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index 08dc2f78b3151..8ac6acad7fd63 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -13,12 +13,6 @@ export interface IVpcSubnet extends IConstruct { */ readonly subnetId: string; - /** - * The id of the route table associated with this subnet. - * Not available for an imported subnet. - */ - readonly routeTableId: string; - /** * Dependable that can be depended upon to force internet connectivity established on the VPC */ diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index a0f320cce602d..026b0000b046e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -390,7 +390,7 @@ export class VpcNetwork extends VpcNetworkBase { // Propagate routes on route tables associated with private subnets const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { - routeTableIds: this.privateSubnets.map(subnet => subnet.routeTableId), + routeTableIds: (this.privateSubnets as VpcPrivateSubnet[]).map(subnet => subnet.routeTableId), vpnGatewayId: this.vpnGatewayId }); @@ -744,14 +744,12 @@ class ImportedVpcSubnet extends cdk.Construct implements IVpcSubnet { public readonly internetConnectivityEstablished: cdk.IDependable = new cdk.ConcreteDependable(); public readonly availabilityZone: string; public readonly subnetId: string; - public readonly routeTableId: string; constructor(scope: cdk.Construct, id: string, private readonly props: VpcSubnetImportProps) { super(scope, id); this.subnetId = props.subnetId; this.availabilityZone = props.availabilityZone; - this.routeTableId = ''; } public export() { From 0692784acfcd9e070dba8bec4716d43a132cc827 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 19:22:16 +0100 Subject: [PATCH 05/14] Better default for vpnGateway --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 026b0000b046e..abd74971763d9 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -120,7 +120,7 @@ export interface VpcNetworkProps { /** * Indicates whether a VPN gateway should be created and attached to this VPC. * - * @default true when vpnConnections is specified, false otherwise. + * @default true when vpnGatewayAsn or vpnConnections is specified. */ vpnGateway?: boolean; @@ -375,7 +375,7 @@ export class VpcNetwork extends VpcNetworkBase { throw new Error('Cannot specify `vpnConnections` when `vpnGateway` is set to false.'); } - if (props.vpnGateway || props.vpnConnections) { + if (props.vpnGateway || props.vpnConnections || props.vpnGatewayAsn) { const vpnGateway = new CfnVPNGateway(this, 'VpnGateway', { amazonSideAsn: props.vpnGatewayAsn, type: VpnConnectionType.IPsec1 From 657ca9603b2fce1eaa837ff14ea548ec119d0e9c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 21:41:48 +0100 Subject: [PATCH 06/14] Update README --- packages/@aws-cdk/aws-ec2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 59cbbfd91b417..4cda8dcbc4cfa 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -325,7 +325,7 @@ const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { }); ``` -To export a VPC that can accept VPN connections, set `vpnGateway` to `true`: +To create a VPC that can accept VPN connections, set `vpnGateway` to `true`: ```ts const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { From b0cf686708e9f287b312b34250accfbdb408c15a Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 21:43:11 +0100 Subject: [PATCH 07/14] Add dummy member to VpnConnectionType enum --- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 963fa78c16de3..0cb464c7bfce8 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -77,7 +77,13 @@ export enum VpnConnectionType { /** * The IPsec 1 VPN connection type. */ - IPsec1 = 'ipsec.1' + 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 { From a589734ac974219ce04c575c00dce52e289c0dbc Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 22:29:21 +0100 Subject: [PATCH 08/14] Style --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 6 +++--- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index abd74971763d9..40c336dabbce7 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -400,9 +400,9 @@ export class VpcNetwork extends VpcNetworkBase { routePropagation.node.addDependency(attachment); const vpnConnections = props.vpnConnections || {}; - Object.keys(vpnConnections).forEach(cId => { - this.addVpnConnection(cId, vpnConnections[cId]); - }); + for (const [connectionId, connection] of Object.entries(vpnConnections)) { + this.addVpnConnection(connectionId, connection); + } } } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 0cb464c7bfce8..8c4a8e5f59fc2 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -23,6 +23,7 @@ export interface IVpnConnection extends cdk.IConstruct { */ readonly customerGatewayAsn: number; } + export interface VpnTunnelOption { /** * The pre-shared key (PSK) to establish initial authentication between the virtual From 3aaa1fef77f55d3585fe8e7c3165117e4d93a2b1 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 22:35:05 +0100 Subject: [PATCH 09/14] Better error handling --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 4 ++-- packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 40c336dabbce7..e9b5bbe02f37d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -371,8 +371,8 @@ export class VpcNetwork extends VpcNetworkBase { }); } - if (props.vpnConnections && props.vpnGateway === false) { - throw new Error('Cannot specify `vpnConnections` when `vpnGateway` is set to false.'); + 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) { diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts index 1940b51b48ce2..cd6e8092b59c1 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -113,6 +113,18 @@ export = { } }), /`vpnConnections`.+`vpnGateway`.+false/); + test.done(); + }, + + 'fails when specifying vpnGatewayAsn with vpnGateway set to false'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: false, + vpnGatewayAsn: 65000, + }), /`vpnGatewayAsn`.+`vpnGateway`.+false/); + test.done(); } }; From b456cc4403f573646f494ab7ca43c1e1d2e12e50 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 22:35:50 +0100 Subject: [PATCH 10/14] Call addVpnConnection in integ test --- packages/@aws-cdk/aws-ec2/test/integ.vpn.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts index b6a6fe60f4b53..319e0ca36ec45 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts @@ -4,20 +4,21 @@ import ec2 = require('../lib'); const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-ec2-vpn'); -new ec2.VpcNetwork(stack, 'MyVpc', { +const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { cidr: '10.10.0.0/16', vpnConnections: { Dynamic: { // Dynamic routing ip: '52.85.255.164' - }, - Static: { // Static routing - ip: '52.85.255.197', - staticRoutes: [ - '192.168.10.0/24', - '192.168.20.0/24' - ] } } }); +vpc.addVpnConnection('Static', { // Static routing + ip: '52.85.255.197', + staticRoutes: [ + '192.168.10.0/24', + '192.168.20.0/24' + ] +}); + app.run(); From 8d30a7f0114e1ce9b89e1afe95ca322cd97c65bc Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 27 Feb 2019 23:09:35 +0100 Subject: [PATCH 11/14] Make dummy really dummy --- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 8c4a8e5f59fc2..1ea22b4e783a2 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -84,7 +84,7 @@ export enum VpnConnectionType { * Dummy member * TODO: remove once https://github.com/awslabs/jsii/issues/231 is fixed */ - Dummy = 'ipsec.1' + Dummy = 'dummy' } export class VpnConnection extends cdk.Construct implements IVpnConnection { From c77f6144f377f842bde2cc909493d7d55735c9a9 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Feb 2019 00:41:29 +0100 Subject: [PATCH 12/14] Add option to customize route propagation --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 22 ++++++- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 72 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index e9b5bbe02f37d..af1ba62bebbbb 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -137,6 +137,13 @@ export interface VpcNetworkProps { * @default no connections */ vpnConnections?: { [id: string]: VpnConnectionOptions } + + /** + * Where to propagate VPN routes. + * + * @default on the route tables associated with private subnets + */ + vpnRoutePropagation?: SubnetType[] } /** @@ -388,9 +395,20 @@ export class VpcNetwork extends VpcNetworkBase { this.vpnGatewayId = vpnGateway.vpnGatewayName; - // Propagate routes on route tables associated with private subnets + // Propagate routes on route tables associated with the right subnets + const vpnRoutePropagation = props.vpnRoutePropagation || [SubnetType.Private]; + let subnets: IVpcSubnet[] = []; + if (vpnRoutePropagation.includes(SubnetType.Public)) { + subnets = [...subnets, ...this.publicSubnets]; + } + if (vpnRoutePropagation.includes(SubnetType.Private)) { + subnets = [...subnets, ...this.privateSubnets]; + } + if (vpnRoutePropagation.includes(SubnetType.Isolated)) { + subnets = [...subnets, ...this.isolatedSubnets]; + } const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { - routeTableIds: (this.privateSubnets as VpcPrivateSubnet[]).map(subnet => subnet.routeTableId), + routeTableIds: (subnets as VpcSubnet[]).map(subnet => subnet.routeTableId), vpnGatewayId: this.vpnGatewayId }); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 9af1d70638733..2bfb0e03a54f0 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -361,6 +361,78 @@ export = { } })); + test.done(); + }, + 'with a vpn gateway and route propagation on isolated subnets'(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'VPC', { + subnetConfiguration: [ + { subnetType: SubnetType.Private, name: 'Private' }, + { subnetType: SubnetType.Isolated, name: 'Isolated' }, + ], + vpnGateway: true, + vpnRoutePropagation: [SubnetType.Isolated] + }); + + expect(stack).to(haveResource('AWS::EC2::VPNGatewayRoutePropagation', { + RouteTableIds: [ + { + Ref: 'VPCIsolatedSubnet1RouteTableEB156210' + }, + { + Ref: 'VPCIsolatedSubnet2RouteTable9B4F78DC' + }, + { + Ref: 'VPCIsolatedSubnet3RouteTableCB6A1FDA' + } + ], + VpnGatewayId: { + Ref: 'VPCVpnGatewayB5ABAE68' + } + })); + + test.done(); + }, + 'with a vpn gateway and route propagation on private and isolated subnets'(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'VPC', { + subnetConfiguration: [ + { subnetType: SubnetType.Private, name: 'Private' }, + { subnetType: SubnetType.Isolated, name: 'Isolated' }, + ], + vpnGateway: true, + vpnRoutePropagation: [ + SubnetType.Private, + SubnetType.Isolated + ] + }); + + expect(stack).to(haveResource('AWS::EC2::VPNGatewayRoutePropagation', { + RouteTableIds: [ + { + Ref: 'VPCPrivateSubnet1RouteTableBE8A6027' + }, + { + Ref: 'VPCPrivateSubnet2RouteTable0A19E10E' + }, + { + Ref: 'VPCPrivateSubnet3RouteTable192186F8' + }, + { + Ref: 'VPCIsolatedSubnet1RouteTableEB156210' + }, + { + Ref: 'VPCIsolatedSubnet2RouteTable9B4F78DC' + }, + { + Ref: 'VPCIsolatedSubnet3RouteTableCB6A1FDA' + } + ], + VpnGatewayId: { + Ref: 'VPCVpnGatewayB5ABAE68' + } + })); + test.done(); } From cbf023528c45621a516f4e15440f28b7bb121035 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Feb 2019 09:51:57 +0100 Subject: [PATCH 13/14] Add tunnel options validation and tests --- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 68 +++++++- .../aws-ec2/test/integ.vpn.expected.json | 5 + packages/@aws-cdk/aws-ec2/test/integ.vpn.ts | 7 +- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 27 ++++ packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 151 ++++++++++++++++-- 5 files changed, 242 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 1ea22b4e783a2..c35993f8a35c1 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -27,15 +27,21 @@ export interface IVpnConnection extends cdk.IConstruct { export interface VpnTunnelOption { /** * The pre-shared key (PSK) to establish initial authentication between the virtual - * private gateway and customer gateway. + * private gateway and customer gateway. Allowed characters are alphanumeric characters + * and ._. Must be between 8 and 64 characters in length and cannot start with zero (0). + * + * @default an Amazon generated pre-shared key */ - presharedKey: string; + 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. + * A size /30 CIDR block from the 169.254.0.0/16 range. + * + * @default an Amazon generated inside IP CIDR */ - tunnelInsideCidr: string; + tunnelInsideCidr?: string; } export interface VpnConnectionOptions { @@ -59,9 +65,12 @@ export interface VpnConnectionOptions { staticRoutes?: string[]; /** - * Tunnel options for the VPN connection. + * The tunnel options for the VPN connection. At most two elements (one per tunnel). + * Duplicates not allowed. + * + * @default Amazon generated tunnel options */ - vpnTunnelOptions?: VpnTunnelOption[]; + tunnelOptions?: VpnTunnelOption[]; } export interface VpnConnectionProps extends VpnConnectionOptions { @@ -100,6 +109,10 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { throw new Error('Cannot create a VPN connection when VPC has no VPN gateway.'); } + if (!IP_REGEX.test(props.ip)) { + throw new Error(`The \`ip\` ${props.ip} is invalid.`); + } + const type = VpnConnectionType.IPsec1; const bgpAsn = props.asn || 65000; @@ -113,12 +126,43 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { this.customerGatewayAsn = bgpAsn; this.customerGatewayIp = props.ip; + // Validate tunnel options + if (props.tunnelOptions) { + if (props.tunnelOptions.length > 2) { + throw new Error('Cannot specify more than two `tunnelOptions`'); + } + + if (props.tunnelOptions.length === 2 && props.tunnelOptions[0].tunnelInsideCidr === props.tunnelOptions[1].tunnelInsideCidr) { + throw new Error(`Same ${props.tunnelOptions[0].tunnelInsideCidr} \`tunnelInsideCidr\` cannot be used for both tunnels.`); + } + + props.tunnelOptions.forEach((options, index) => { + if (options.preSharedKey && !/^[a-zA-Z1-9._][a-zA-Z\d._]{7,63}$/.test(options.preSharedKey)) { + // tslint:disable:max-line-length + throw new Error(`The \`preSharedKey\` ${options.preSharedKey} for tunnel ${index + 1} is invalid. Allowed characters are alphanumeric characters and ._. Must be between 8 and 64 characters in length and cannot start with zero (0).`); + // tslint:enable:max-line-length + } + + if (options.tunnelInsideCidr) { + if (RESERVED_TUNNEL_INSIDE_CIDR.includes(options.tunnelInsideCidr)) { + throw new Error(`The \`tunnelInsideCidr\` ${options.tunnelInsideCidr} for tunnel ${index + 1} is a reserved inside CIDR.`); + } + + if (!/^169\.254\.\d{1,3}\.\d{1,3}\/30$/.test(options.tunnelInsideCidr)) { + // tslint:disable:max-line-length + throw new Error(`The \`tunnelInsideCidr\` ${options.tunnelInsideCidr} for tunnel ${index + 1} is not a size /30 CIDR block from the 169.254.0.0/16 range.`); + // tslint:enable:max-line-length + } + } + }); + } + const vpnConnection = new CfnVPNConnection(this, 'Resource', { type, customerGatewayId: customerGateway.customerGatewayName, staticRoutesOnly: props.staticRoutes ? true : false, vpnGatewayId: props.vpc.vpnGatewayId, - vpnTunnelOptionsSpecifications: props.vpnTunnelOptions + vpnTunnelOptionsSpecifications: props.tunnelOptions }); this.vpnId = vpnConnection.vpnConnectionName; @@ -133,3 +177,15 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { } } } + +export const IP_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; + +export const RESERVED_TUNNEL_INSIDE_CIDR = [ + '169.254.0.0/30', + '169.254.1.0/30', + '169.254.2.0/30', + '169.254.3.0/30', + '169.254.4.0/30', + '169.254.5.0/30', + '169.254.169.252/30' +]; diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json index 551e467ea2e1f..3d69d68e59a76 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json @@ -571,6 +571,11 @@ "Key": "Name", "Value": "aws-cdk-ec2-vpn/MyVpc" } + ], + "VpnTunnelOptionsSpecifications": [ + { + "PreSharedKey": "secretkey1234" + } ] } }, diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts index 319e0ca36ec45..7d1db5544e51d 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts @@ -8,7 +8,12 @@ const vpc = new ec2.VpcNetwork(stack, 'MyVpc', { cidr: '10.10.0.0/16', vpnConnections: { Dynamic: { // Dynamic routing - ip: '52.85.255.164' + ip: '52.85.255.164', + tunnelOptions: [ + { + preSharedKey: 'secretkey1234' + } + ] } } }); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 2bfb0e03a54f0..23a3b79056506 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -433,6 +433,33 @@ export = { } })); + test.done(); + }, + 'fails when specifying vpnConnections with vpnGateway set to false'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: false, + vpnConnections: { + VpnConnection: { + asn: 65000, + ip: '192.0.2.1' + } + } + }), /`vpnConnections`.+`vpnGateway`.+false/); + + test.done(); + }, + 'fails when specifying vpnGatewayAsn with vpnGateway set to false'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnGateway: false, + vpnGatewayAsn: 65000, + }), /`vpnGatewayAsn`.+`vpnGateway`.+false/); + test.done(); } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts index cd6e8092b59c1..4741adf86b91e 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -85,6 +85,44 @@ export = { test.done(); }, + 'with tunnel options'(test: Test) { + // GIVEN + const stack = new Stack(); + + new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + preSharedKey: 'secretkey1234', + tunnelInsideCidr: '169.254.10.0/30' + } + ] + } + } + }); + + expect(stack).to(haveResource('AWS::EC2::VPNConnection', { + CustomerGatewayId: { + Ref: 'VpcNetworkVpnConnectionCustomerGateway8B56D9AF' + }, + Type: 'ipsec.1', + VpnGatewayId: { + Ref: 'VpcNetworkVpnGateway501295FA' + }, + StaticRoutesOnly: false, + VpnTunnelOptionsSpecifications: [ + { + PreSharedKey: 'secretkey1234', + TunnelInsideCidr: '169.254.10.0/30' + } + ] + })); + + test.done(); + }, + 'fails when vpc has no vpn gateway'(test: Test) { // GIVEN const stack = new Stack(); @@ -99,31 +137,126 @@ export = { test.done(); }, - 'fails when specifying vpnConnections with vpnGateway set to false'(test: Test) { + 'fails when ip is invalid'(test: Test) { // GIVEN const stack = new Stack(); test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { - vpnGateway: false, vpnConnections: { VpnConnection: { - asn: 65000, - ip: '192.0.2.1' + ip: '192.0.2.256' + } + } + }), /`ip`.+invalid/); + + test.done(); + }, + + 'fails when specifying more than two tunnel options'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + preSharedKey: 'secretkey1234', + }, + { + preSharedKey: 'secretkey1234', + }, + { + preSharedKey: 'secretkey1234', + } + ] + } + } + }), /two.+`tunnelOptions`/); + + test.done(); + }, + + 'fails with duplicate tunnel inside cidr'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + tunnelInsideCidr: '169.254.10.0/30', + }, + { + tunnelInsideCidr: '169.254.10.0/30', + } + ] } } - }), /`vpnConnections`.+`vpnGateway`.+false/); + }), /`tunnelInsideCidr`.+both tunnels/); test.done(); }, - 'fails when specifying vpnGatewayAsn with vpnGateway set to false'(test: Test) { + 'fails when specifying an invalid pre-shared key'(test: Test) { // GIVEN const stack = new Stack(); test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { - vpnGateway: false, - vpnGatewayAsn: 65000, - }), /`vpnGatewayAsn`.+`vpnGateway`.+false/); + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + preSharedKey: '0invalid', + } + ] + } + } + }), /`preSharedKey`/); + + test.done(); + }, + + 'fails when specifying a reserved tunnel inside cidr'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + tunnelInsideCidr: '169.254.1.0/30', + } + ] + } + } + }), /`tunnelInsideCidr`.+reserved/); + + test.done(); + }, + + 'fails when specifying an invalid tunnel inside cidr'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.1', + tunnelOptions: [ + { + tunnelInsideCidr: '169.200.10.0/30', + } + ] + } + } + }), /`tunnelInsideCidr`.+size/); test.done(); } From bddf05fd906ca9e94994fce4ec2dbe493b9305e7 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Feb 2019 17:06:11 +0100 Subject: [PATCH 14/14] Use net instead of regex for ip validation --- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 7 +++---- packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index c35993f8a35c1..bab383c4b8aee 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import net = require('net'); import { CfnCustomerGateway, CfnVPNConnection, CfnVPNConnectionRoute } from './ec2.generated'; import { IVpcNetwork } from './vpc-ref'; @@ -109,8 +110,8 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { throw new Error('Cannot create a VPN connection when VPC has no VPN gateway.'); } - if (!IP_REGEX.test(props.ip)) { - throw new Error(`The \`ip\` ${props.ip} is invalid.`); + if (!net.isIPv4(props.ip)) { + throw new Error(`The \`ip\` ${props.ip} is not a valid IPv4 address.`); } const type = VpnConnectionType.IPsec1; @@ -178,8 +179,6 @@ export class VpnConnection extends cdk.Construct implements IVpnConnection { } } -export const IP_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; - export const RESERVED_TUNNEL_INSIDE_CIDR = [ '169.254.0.0/30', '169.254.1.0/30', diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts index 4741adf86b91e..6dcfd3e92e29b 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpn.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -147,7 +147,7 @@ export = { ip: '192.0.2.256' } } - }), /`ip`.+invalid/); + }), /`ip`.+IPv4/); test.done(); },