From e1506489318e019b93570afe5abf0920b70fd26e Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 4 Mar 2019 12:06:57 +0100 Subject: [PATCH] feat(ec2): add support for vpn connections (#1899) Add support for Site-to-Site VPN connections to VPC networks. When VPN connections are specified, a VPN gateway is automatically created and attached to the VPC. By default, routes are propagated on the route tables associated with the private subnets. Propagation to routes tables associated with public and/or isolated subnets is supported. Update VPC context provider to also import vpnGatewayId. References awslabs/jsii#231 --- packages/@aws-cdk/aws-ec2/README.md | 38 ++ packages/@aws-cdk/aws-ec2/lib/index.ts | 1 + packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 31 + packages/@aws-cdk/aws-ec2/lib/vpc.ts | 86 ++- packages/@aws-cdk/aws-ec2/lib/vpn.ts | 190 ++++++ .../aws-ec2/test/integ.vpn.expected.json | 634 ++++++++++++++++++ packages/@aws-cdk/aws-ec2/test/integ.vpn.ts | 29 + packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 139 ++++ packages/@aws-cdk/aws-ec2/test/test.vpn.ts | 263 ++++++++ packages/@aws-cdk/cx-api/lib/context/vpc.ts | 7 +- .../aws-cdk/lib/context-providers/vpcs.ts | 24 +- 11 files changed, 1437 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..4cda8dcbc4cfa 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -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. 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..8ac6acad7fd63 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 { VpnConnection, VpnConnectionOptions } from './vpn'; export interface IVpcSubnet extends IConstruct { /** @@ -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 */ @@ -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. */ @@ -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 */ @@ -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 */ @@ -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 { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 792093a64ba4c..af1ba62bebbbb 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 { VpnConnectionOptions, VpnConnectionType } from './vpn'; /** * Name tag constant @@ -115,6 +116,34 @@ 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 } + + /** + * Where to propagate VPN routes. + * + * @default on the route tables associated with private subnets + */ + vpnRoutePropagation?: SubnetType[] } /** @@ -250,6 +279,11 @@ export class VpcNetwork extends VpcNetworkBase { */ public readonly availabilityZones: string[]; + /** + * Identifier for the VPN gateway + */ + public readonly vpnGatewayId?: string; + /** * The VPC resource */ @@ -343,6 +377,51 @@ 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 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: (subnets as VpcSubnet[]).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); + + const vpnConnections = props.vpnConnections || {}; + for (const [connectionId, connection] of Object.entries(vpnConnections)) { + this.addVpnConnection(connectionId, connection); + } + } } /** @@ -355,6 +434,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 +603,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 +733,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'); 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..bab383c4b8aee --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -0,0 +1,190 @@ +import cdk = require('@aws-cdk/cdk'); +import net = require('net'); +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. 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; + + /** + * 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; +} + +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[]; + + /** + * The tunnel options for the VPN connection. At most two elements (one per tunnel). + * Duplicates not allowed. + * + * @default Amazon generated tunnel options + */ + tunnelOptions?: VpnTunnelOption[]; +} + +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 = 'dummy' +} + +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.'); + } + + if (!net.isIPv4(props.ip)) { + throw new Error(`The \`ip\` ${props.ip} is not a valid IPv4 address.`); + } + + 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; + + // 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.tunnelOptions + }); + + this.vpnId = vpnConnection.vpnConnectionName; + + if (props.staticRoutes) { + props.staticRoutes.forEach(route => { + new CfnVPNConnectionRoute(this, `Route${route.replace(/[^\d]/g, '')}`, { + destinationCidrBlock: route, + vpnConnectionId: this.vpnId + }); + }); + } + } +} + +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 new file mode 100644 index 0000000000000..3d69d68e59a76 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json @@ -0,0 +1,634 @@ +{ + "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" + } + ], + "VpnTunnelOptionsSpecifications": [ + { + "PreSharedKey": "secretkey1234" + } + ] + } + }, + "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..7d1db5544e51d --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.ts @@ -0,0 +1,29 @@ +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', + vpnConnections: { + Dynamic: { // Dynamic routing + ip: '52.85.255.164', + tunnelOptions: [ + { + preSharedKey: 'secretkey1234' + } + ] + } + } +}); + +vpc.addVpnConnection('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.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 60c6a28b38276..23a3b79056506 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -323,6 +323,145 @@ 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(); + }, + '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(); + }, + '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 new file mode 100644 index 0000000000000..6dcfd3e92e29b --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/test.vpn.ts @@ -0,0 +1,263 @@ +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 + new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + 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 + 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: 'VpcNetworkstaticCustomerGatewayAF2651CC' + }, + Type: 'ipsec.1', + VpnGatewayId: { + Ref: 'VpcNetworkVpnGateway501295FA' + }, + StaticRoutesOnly: true, + })); + + expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { + DestinationCidrBlock: '192.168.10.0/24', + VpnConnectionId: { + Ref: 'VpcNetworkstaticE33EA98C' + } + })); + + expect(stack).to(haveResource('AWS::EC2::VPNConnectionRoute', { + DestinationCidrBlock: '192.168.20.0/24', + VpnConnectionId: { + Ref: 'VpcNetworkstaticE33EA98C' + } + })); + + 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(); + + const vpc = new VpcNetwork(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(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: '192.0.2.256' + } + } + }), /`ip`.+IPv4/); + + 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', + } + ] + } + } + }), /`tunnelInsideCidr`.+both tunnels/); + + test.done(); + }, + + 'fails when specifying an invalid pre-shared key'(test: Test) { + // GIVEN + const stack = new Stack(); + + test.throws(() => new VpcNetwork(stack, 'VpcNetwork', { + 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(); + } +}; 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, }; } }