From 4a76287d47b12cb8e1979d04a876e8abbabca36b Mon Sep 17 00:00:00 2001 From: Mike Cowgill Date: Wed, 10 Oct 2018 01:33:41 -0700 Subject: [PATCH] feat(aws-ec2): allow configuring subnets for NAT gateway (#874) Adds a `natGatewayPlacement` property which can be used to pick the specific (public) subnets for placing the NAT gateways. Useful if you have multiple public subnets with different ingress and egress NACLs. --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 74 ++++++++++++++-------- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 65 ++++++++++++++++++- 2 files changed, 108 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 11c4f9c918cb4..41bc9b97bcfdc 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1,8 +1,8 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ec2.generated'; import { NetworkBuilder } from './network-util'; -import { DEFAULT_SUBNET_NAME, subnetId } from './util'; -import { SubnetType, VpcNetworkRef, VpcSubnetRef } from './vpc-ref'; +import { DEFAULT_SUBNET_NAME, subnetId } from './util'; +import { SubnetType, VpcNetworkRef, VpcPlacementStrategy, VpcSubnetRef } from './vpc-ref'; /** * Name tag constant @@ -61,17 +61,24 @@ export interface VpcNetworkProps { maxAZs?: number; /** - * Define the maximum number of NAT Gateways for this VPC + * The number of NAT Gateways to create. * - * Setting this number enables a VPC to trade availability for the cost of - * running a NAT Gateway. For example, if set this to 1 and your subnet - * configuration is for 3 Public subnets with natGateway = `true` then only - * one of the Public subnets will have a gateway and all Private subnets - * will route to this NAT Gateway. + * For example, if set this to 1 and your subnet configuration is for 3 Public subnets then only + * one of the Public subnets will have a gateway and all Private subnets will route to this NAT Gateway. * @default maxAZs */ natGateways?: number; + /** + * Configures the subnets which will have NAT Gateways + * + * You can pick a specific group of subnets by specifying the group name; + * the picked subnets must be public subnets. + * + * @default All public subnets + */ + natGatewayPlacement?: VpcPlacementStrategy; + /** * Configure the subnets to build for each AZ * @@ -231,13 +238,6 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { */ public readonly tags: cdk.TagManager; - /** - * Maximum Number of NAT Gateways used to control cost - * - * @default {VpcNetworkProps.maxAZs} - */ - private readonly natGateways: number; - /** * The VPC resource */ @@ -301,11 +301,6 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { this.dependencyElements.push(this.resource); this.subnetConfiguration = ifUndefined(props.subnetConfiguration, VpcNetwork.DEFAULT_SUBNETS); - const useNatGateway = this.subnetConfiguration.filter( - subnet => (subnet.subnetType === SubnetType.Private)).length > 0; - this.natGateways = ifUndefined(props.natGateways, - useNatGateway ? this.availabilityZones.length : 0); - // subnetConfiguration and natGateways must be set before calling createSubnets this.createSubnets(); @@ -321,11 +316,14 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { internetGatewayId: igw.ref, vpcId: this.resource.ref }); + this.dependencyElements.push(igw, att); + (this.publicSubnets as VpcPublicSubnet[]).forEach(publicSubnet => { publicSubnet.addDefaultIGWRouteEntry(igw.ref); }); - this.dependencyElements.push(igw, att); + // if gateways are needed create them + this.createNatGateways(props.natGateways, props.natGatewayPlacement); (this.privateSubnets as VpcPrivateSubnet[]).forEach((privateSubnet, i) => { let ngwId = this.natGatewayByAZ[privateSubnet.availabilityZone]; @@ -346,6 +344,32 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { return this.resource.getAtt("CidrBlock").toString(); } + private createNatGateways(gateways?: number, placement?: VpcPlacementStrategy): void { + const useNatGateway = this.subnetConfiguration.filter( + subnet => (subnet.subnetType === SubnetType.Private)).length > 0; + + const natCount = ifUndefined(gateways, + useNatGateway ? this.availabilityZones.length : 0); + + let natSubnets: VpcPublicSubnet[]; + if (placement) { + const subnets = this.subnets(placement); + for (const sub of subnets) { + if (!this.isPublicSubnet(sub)) { + throw new Error(`natGatewayPlacement ${placement} contains non public subnet ${sub}`); + } + } + natSubnets = subnets as VpcPublicSubnet[]; + } else { + natSubnets = this.publicSubnets as VpcPublicSubnet[]; + } + + natSubnets = natSubnets.slice(0, natCount); + for (const sub of natSubnets) { + this.natGatewayByAZ[sub.availabilityZone] = sub.addNatGateway(); + } + } + /** * createSubnets creates the subnets specified by the subnet configuration * array or creates the `DEFAULT_SUBNETS` configuration @@ -386,12 +410,6 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { switch (subnetConfig.subnetType) { case SubnetType.Public: const publicSubnet = new VpcPublicSubnet(this, name, subnetProps); - if (this.natGateways > 0) { - const ngwArray = Array.from(Object.values(this.natGatewayByAZ)); - if (ngwArray.length < this.natGateways) { - this.natGatewayByAZ[zone] = publicSubnet.addNatGateway(); - } - } this.publicSubnets.push(publicSubnet); break; case SubnetType.Private: @@ -561,5 +579,5 @@ export class VpcPrivateSubnet extends VpcSubnet { } function ifUndefined(value: T | undefined, defaultValue: T): T { - return value !== undefined ? value : defaultValue; + return value !== undefined ? value : defaultValue; } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index f571bfb6d7e48..403563c2875f5 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -251,17 +251,76 @@ export = { }, "with natGateway set to 1"(test: Test) { const stack = getTestStack(); - new VpcNetwork(stack, 'VPC', { natGateways: 1 }); + new VpcNetwork(stack, 'VPC', { + natGateways: 1, + }); expect(stack).to(countResources("AWS::EC2::Subnet", 6)); expect(stack).to(countResources("AWS::EC2::Route", 6)); - expect(stack).to(countResources("AWS::EC2::Subnet", 6)); expect(stack).to(countResources("AWS::EC2::NatGateway", 1)); expect(stack).to(haveResource("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', NatGatewayId: { }, })); test.done(); - } + }, + 'with natGateway subnets defined'(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'VPC', { + subnetConfiguration: [ + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + }, + { + cidrMask: 24, + name: 'egress', + subnetType: SubnetType.Public, + }, + { + cidrMask: 24, + name: 'private', + subnetType: SubnetType.Private, + }, + ], + natGatewayPlacement: { + subnetName: 'egress' + }, + }); + expect(stack).to(countResources("AWS::EC2::NatGateway", 3)); + for (let i = 1; i < 4; i++) { + expect(stack).to(haveResource("AWS::EC2::NatGateway", { + Tags: [ + { + Key: 'Name', + Value: `VPC/egressSubnet${i}`, + } + ] + })); + } + test.done(); + }, + 'with mis-matched nat and subnet configs it throws'(test: Test) { + const stack = getTestStack(); + test.throws(() => new VpcNetwork(stack, 'VPC', { + subnetConfiguration: [ + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + }, + { + cidrMask: 24, + name: 'private', + subnetType: SubnetType.Private, + }, + ], + natGatewayPlacement: { + subnetName: 'notthere', + }, + })); + test.done(); + }, },