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, }; } }