From 1754435718af77e52a23f8385af15ba959baa59c Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Tue, 26 Mar 2019 01:06:51 +0100 Subject: [PATCH 1/7] feat(VpcNetwork): Add ability to have subnet groups in configuration --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 130 +++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 9465fdcf6dfc7..5c0386bf111f2 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -3,7 +3,7 @@ import { ConcreteDependable, IDependable } from '@aws-cdk/cdk'; 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 { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util'; import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider'; import { IVpcNetwork, IVpcSubnet, SubnetSelection, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcSubnetImportProps } from './vpc-ref'; import { VpnConnectionOptions, VpnConnectionType } from './vpn'; @@ -115,7 +115,7 @@ export interface VpcNetworkProps { * @default the VPC CIDR will be evenly divided between 1 public and 1 * private subnet per AZ */ - subnetConfiguration?: SubnetConfiguration[]; + subnetConfiguration?: Array; /** * Indicates whether a VPN gateway should be created and attached to this VPC. @@ -165,6 +165,13 @@ export enum DefaultInstanceTenancy { * Specify configuration parameters for a VPC to be built */ export interface SubnetConfiguration { + /** + * The name of the subnetGroup to which this subnet belongs to + * + * The corresponding subnet will be tagged with this name + */ + subnetGroup?: string; + /** * The CIDR Mask or the number of leading 1 bits in the routing mask * @@ -183,12 +190,63 @@ export interface SubnetConfiguration { /** * The common Logical Name for the `VpcSubnet` * - * Thi name will be suffixed with an integer correlating to a specific + * This name will be suffixed with an integer correlating to a specific * availability zone. */ name: string; } +/** + * Specify configuration parameters for a VPC to be built + */ +export interface SubnetGroupConfiguration { + /** + * The name of the subnetGroup + * + * All subnets withing this group will be tagged with this name + */ + subnetGroupName: string; + + /** + * The CIDR Mask or the number of leading 1 bits in the routing mask + * + * Valid values are 16 - 28 + */ + cidrMask: number; + + /** + * Configure the subnets to build for each AZ + * + * The subnets are constructed in the context of the VPC and SubnetGroup so you only need + * specify the configuration. The VPC details (VPC ID, specific CIDR, + * specific AZ will be calculated during creation) + * + * For example if you want 3 private subnets in the SubnetGroup + * in each AZ provide the following: + * subnetConfiguration: [ + * { + * name: 'applicationA', + * subnetType: SubnetType.Public, + * }, + * { + * name: 'applicationB', + * subnetType: SubnetType.Private, + * }, + * { + * name: 'applicationC', + * subnetType: SubnetType.Private, + * } + * ] + * + */ + subnetConfiguration?: SubnetConfiguration[]; + + /** + * The maximum number of subnets that can be in this subnet group + */ + maxSubnets: number; +} + /** * VpcNetwork deploys an AWS VPC, with public and private subnets per Availability Zone. * For example: @@ -343,7 +401,14 @@ export class VpcNetwork extends VpcNetworkBase { this.vpcId = this.resource.vpcId; - this.subnetConfiguration = ifUndefined(props.subnetConfiguration, VpcNetwork.DEFAULT_SUBNETS); + // Expand any subnetGroup to subnets + if (props.subnetConfiguration) { + props.subnetConfiguration = new Array().concat( + ...props.subnetConfiguration.map(this.subnetGroupToSubnets) + ); + } + + this.subnetConfiguration = ifUndefined(props.subnetConfiguration as SubnetConfiguration[], VpcNetwork.DEFAULT_SUBNETS); // subnetConfiguration and natGateways must be set before calling createSubnets this.createSubnets(); @@ -462,7 +527,7 @@ export class VpcNetwork extends VpcNetworkBase { } natSubnets = subnets as VpcPublicSubnet[]; } else { - natSubnets = this.publicSubnets as VpcPublicSubnet[]; + natSubnets = this.publicSubnets as VpcPublicSubnet[]; } natSubnets = natSubnets.slice(0, natCount); @@ -473,6 +538,44 @@ export class VpcNetwork extends VpcNetworkBase { } } + /** + * subnetGroupToSubnets expands a SubnetGroupConfiguration into an array of + * SubnetConfiguration. It appends any stub subnets at the end to ensure that the + * subnet ip space is pre allocated. If argument is a SubnetConfiguration, this + * is returned inside a single value array but is untouched + */ + private subnetGroupToSubnets(subnetOrSubnetGroup: SubnetConfiguration | SubnetGroupConfiguration): SubnetConfiguration[] { + if (!(subnetOrSubnetGroup as SubnetGroupConfiguration).subnetGroupName) { + // Argument is not a subnetGroup, return as is inside an array + return [subnetOrSubnetGroup as SubnetConfiguration]; + } + + const subnetGroup: SubnetGroupConfiguration = subnetOrSubnetGroup as SubnetGroupConfiguration; + subnetGroup.subnetConfiguration = subnetGroup.subnetConfiguration !== undefined ? subnetGroup.subnetConfiguration : []; + + // convert subnets in subnetGroup to an array of subnets + let result = subnetGroup.subnetConfiguration.map(subnet => { + return { + ...subnet, + ...objectIfDefined(subnetGroup.cidrMask, { cidrMask: subnetGroup.cidrMask }), + subnetGroup: subnetGroup.subnetGroupName, + }; + }); + + if (subnetGroup.maxSubnets) { + const remainingGroupSubnets = subnetGroup.maxSubnets - result.length; + if (remainingGroupSubnets < 0) { + throw Error(`Number of segments in group ${subnetGroup.subnetGroupName} is greater than defined maximum`); + } else { + const filler = { + ...objectIfDefined(subnetGroup.cidrMask, { cidrMask: subnetGroup.cidrMask }), + }; + result = result.concat(Array(remainingGroupSubnets).fill(filler)); + } + } + return result; + } + /** * createSubnets creates the subnets specified by the subnet configuration * array or creates the `DEFAULT_SUBNETS` configuration @@ -501,6 +604,11 @@ export class VpcNetwork extends VpcNetworkBase { private createSubnetResources(subnetConfig: SubnetConfiguration, cidrMask: number) { this.availabilityZones.forEach((zone, index) => { + if (!subnetConfig.name) { + // This is a filler subnet of a SubnetGroup - just reserve ip space and return + this.networkBuilder.addSubnet(cidrMask); + return; + } const name = subnetId(subnetConfig.name, index); const subnetProps: VpcSubnetProps = { availabilityZone: zone, @@ -532,14 +640,18 @@ export class VpcNetwork extends VpcNetworkBase { // These values will be used to recover the config upon provider import const includeResourceTypes = [CfnSubnet.resourceTypeName]; - subnet.node.apply(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, {includeResourceTypes})); - subnet.node.apply(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), {includeResourceTypes})); + subnet.node.apply(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, { includeResourceTypes })); + subnet.node.apply(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), { includeResourceTypes })); + if (subnetConfig.subnetGroup) { + subnet.node.apply(new cdk.Tag(SUBNETGROUP_TAG, subnetConfig.subnetGroup, { includeResourceTypes })); + } }); } } const SUBNETTYPE_TAG = 'aws-cdk:subnet-type'; const SUBNETNAME_TAG = 'aws-cdk:subnet-name'; +const SUBNETGROUP_TAG = 'aws-cdk:subnet-group'; function subnetTypeTagValue(type: SubnetType) { switch (type) { @@ -727,6 +839,10 @@ function ifUndefined(value: T | undefined, defaultValue: T): T { return value !== undefined ? value : defaultValue; } +function objectIfDefined(element: T | undefined, defaultValue: object): object { + return element !== undefined ? defaultValue : {}; +} + class ImportedVpcNetwork extends VpcNetworkBase { public readonly vpcId: string; public readonly publicSubnets: IVpcSubnet[]; From eac8651f679724bcfa57c1ff86215e701e19542f Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Tue, 26 Mar 2019 01:15:42 +0100 Subject: [PATCH 2/7] feat(VpcNetwork): Add tests for subnet groups --- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 171 ++++++++++++++++++--- 1 file changed, 152 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 0a7dc1e4f1607..737534d867e30 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -10,7 +10,7 @@ export = { "vpc.vpcId returns a token to the VPC ID"(test: Test) { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - test.deepEqual(stack.node.resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + test.deepEqual(stack.node.resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); test.done(); }, @@ -30,11 +30,11 @@ export = { new VpcNetwork(stack, 'TheVPC'); expect(stack).to( haveResource('AWS::EC2::VPC', - hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) + hasTags([{ Key: 'Name', Value: 'TheVPC' }])) ); expect(stack).to( haveResource('AWS::EC2::InternetGateway', - hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) + hasTags([{ Key: 'Name', Value: 'TheVPC' }])) ); test.done(); }, @@ -109,13 +109,146 @@ export = { "with no subnets defined, the VPC should have an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; - new VpcNetwork(stack, 'TheVPC', { }); + new VpcNetwork(stack, 'TheVPC', {}); expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); test.done(); }, - "with custom subents, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { + "with subnetGroups containing more subnets than maximum should throw an Error"(test: Test) { + const stack = getTestStack(); + test.throws(() => new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + subnetGroupName: 'applicationsGroup', + cidrMask: 24, + maxSubnets: 3, + subnetConfiguration: [ + { name: 'application1', subnetType: SubnetType.Private }, + { name: 'application2', subnetType: SubnetType.Private }, + { name: 'application3', subnetType: SubnetType.Private }, + { name: 'application4', subnetType: SubnetType.Private }, + ], + }], + })); + test.done(); + }, + + "with custom subnets and subnetGroups, the VPC should contain correct number of subnets, an IGW, and a NAT Gateway per AZ "(test: Test) { + const stack = getTestStack(); + const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; + new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + subnetGroupName: 'applicationsGroup', + cidrMask: 24, + maxSubnets: 4, + subnetConfiguration: [ + { name: 'application1', subnetType: SubnetType.Private }, + { name: 'application2', subnetType: SubnetType.Private }, + ], + }, + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + }, + { + cidrMask: 28, + name: 'rds', + subnetType: SubnetType.Isolated, + } + ], + maxAZs: 3 + }); + expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); + expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); + expect(stack).to(countResources("AWS::EC2::Subnet", 12)); + + test.done(); + }, + + "with custom subnets and subnetGroups, the VPC subnets should have correct CidrBlocks"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + subnetGroupName: 'applicationsGroup', + cidrMask: 24, + maxSubnets: 10, + subnetConfiguration: [ + { name: 'application1', subnetType: SubnetType.Private }, + { name: 'application2', subnetType: SubnetType.Private }, + ], + }, + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + }, + ], + maxAZs: 3 + }); + + // Subnets for application1 and applicaton2 + for (let i = 0; i < 6; i++) { + expect(stack).to(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + + // IP's within blocks 10.0.0.0/24 to 10.0.29.0/24 should be allocated by Subnet Group + + // Subnets for ingress + for (let i = 30; i < 33; i++) { + expect(stack).to(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + + test.done(); + }, + + "with subnetGroups without subnetConfiguration, no subnet should have cidrMask within reserved block"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + subnetGroupName: 'applicationsGroup', + cidrMask: 24, + maxSubnets: 10, + }, + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + }, + ], + maxAZs: 3 + }); + + // No Subnets for application group + for (let i = 0; i < 9; i++) { + expect(stack).notTo(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + + // Subnets for ingress + for (let i = 30; i < 33; i++) { + expect(stack).to(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + + test.done(); + }, + + "with custom subnets, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; new VpcNetwork(stack, 'TheVPC', { @@ -233,7 +366,7 @@ export = { } expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: { }, + NatGatewayId: {}, })); test.done(); @@ -251,7 +384,7 @@ export = { } expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: { }, + NatGatewayId: {}, })); test.done(); }, @@ -265,7 +398,7 @@ export = { expect(stack).to(countResources("AWS::EC2::NatGateway", 1)); expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: { }, + NatGatewayId: {}, })); test.done(); }, @@ -478,17 +611,17 @@ export = { 'When tagging': { 'VPC propagated tags will be on subnet, IGW, routetables, NATGW'(test: Test) { const stack = getTestStack(); - const tags = { + const tags = { VpcType: 'Good', }; const noPropTags = { BusinessUnit: 'Marketing', }; - const allTags = {...tags, ...noPropTags}; + const allTags = { ...tags, ...noPropTags }; const vpc = new VpcNetwork(stack, 'TheVPC'); // overwrite to set propagate - vpc.node.apply(new Tag('BusinessUnit', 'Marketing', {includeResourceTypes: [CfnVPC.resourceTypeName]})); + vpc.node.apply(new Tag('BusinessUnit', 'Marketing', { includeResourceTypes: [CfnVPC.resourceTypeName] })); vpc.node.apply(new Tag('VpcType', 'Good')); expect(stack).to(haveResource("AWS::EC2::VPC", hasTags(toCfnTags(allTags)))); const taggables = ['Subnet', 'InternetGateway', 'NatGateway', 'RouteTable']; @@ -504,12 +637,12 @@ export = { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); for (const subnet of vpc.publicSubnets) { - const tag = {Key: 'Name', Value: subnet.node.path}; + const tag = { Key: 'Name', Value: subnet.node.path }; expect(stack).to(haveResource('AWS::EC2::NatGateway', hasTags([tag]))); expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); } for (const subnet of vpc.privateSubnets) { - const tag = {Key: 'Name', Value: subnet.node.path}; + const tag = { Key: 'Name', Value: subnet.node.path }; expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); } test.done(); @@ -518,7 +651,7 @@ export = { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - const tag = {Key: 'Late', Value: 'Adder'}; + const tag = { Key: 'Late', Value: 'Adder' }; expect(stack).notTo(haveResource('AWS::EC2::VPC', hasTags([tag]))); vpc.node.apply(new Tag(tag.Key, tag.Value)); expect(stack).to(haveResource('AWS::EC2::VPC', hasTags([tag]))); @@ -719,17 +852,17 @@ function doImportExportTest(constructFn: (scope: Construct) => VpcNetwork): IVpc return VpcNetwork.import(stack2, 'VPC2', vpc1.export()); } -function toCfnTags(tags: any): Array<{Key: string, Value: string}> { - return Object.keys(tags).map( key => { - return {Key: key, Value: tags[key]}; +function toCfnTags(tags: any): Array<{ Key: string, Value: string }> { + return Object.keys(tags).map(key => { + return { Key: key, Value: tags[key] }; }); } -function hasTags(expectedTags: Array<{Key: string, Value: string}>): (props: any) => boolean { +function hasTags(expectedTags: Array<{ Key: string, Value: string }>): (props: any) => boolean { return (props: any) => { try { const tags = props.Tags; - const actualTags = tags.filter( (tag: {Key: string, Value: string}) => { + const actualTags = tags.filter((tag: { Key: string, Value: string }) => { for (const expectedTag of expectedTags) { if (isSuperObject(expectedTag, tag)) { return true; From fd4557bbd981e1053755050d1cf17aa667ee21cb Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Tue, 26 Mar 2019 01:16:32 +0100 Subject: [PATCH 3/7] feat(VpcNetwork): Document subnet groups in README --- packages/@aws-cdk/aws-ec2/README.md | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index f5b310ddad0e7..cddc127e93bba 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -135,6 +135,111 @@ availability zones will be the following: Any subnet configuration without a `cidrMask` will be counted up and allocated evenly across the remaining IP space. +There are situations where the IP space for a group of subnets having the same +cidrMask's will need to be reserved. This is useful in situations where subnets +would need to be added after the vpc is originally deployed, without causing +IP renumbering for existing subnets. This configuration can be achieved by +defining the subnet group using SubnetGroupConfiguration. + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); + +const vpc = new ec2.VpcNetwork(this, 'TheVPC', { + cidr: '10.0.0.0/16', + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 26, + name: 'Public', + subnetType: SubnetType.Public, + }, + { + subnetGroupName: 'Applications', + cidrMask: 24, + maxSubnets: 10, + subnetConfiguration: [ + { name: 'Application1', subnetType: SubnetType.Private }, + { name: 'Application2', subnetType: SubnetType.Private }, + ], + }, + { + cidrMask: 27, + name: 'Database', + subnetType: SubnetType.Isolated, + } + ], +}); +``` + +The `VpcNetwork` from the above configuration in a Region with three +availability zones will be the following: + * PublicSubnet1: 10.0.0.0/26 + * PublicSubnet2: 10.0.0.64/26 + * PublicSubnet3: 10.0.2.128/26 + * Application1Subnet1: 10.0.1.0/24 + * Application1Subnet2: 10.0.2.0/24 + * Application1Subnet3: 10.0.3.0/24 + * Application2Subnet1: 10.0.4.0/24 + * Application2Subnet2: 10.0.5.0/24 + * Application2Subnet3: 10.0.6.0/24 + * DatabaseSubnet1: 10.0.31.0/27 + * DatabaseSubnet2: 10.0.31.32/27 + * DatabaseSubnet3: 10.0.31.64/27 + +Note that in the above, the space 10.0.1.0/24 to 10.0.30.0/24 is all reserved +for the 'Applications' subnet group. At this point, if another application is +added as follows: + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); + +const vpc = new ec2.VpcNetwork(this, 'TheVPC', { + cidr: '10.0.0.0/16', + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 26, + name: 'Public', + subnetType: SubnetType.Public, + }, + { + subnetGroupName: 'Applications', + cidrMask: 24, + maxSubnets: 10, + subnetConfiguration: [ + { name: 'Application1', subnetType: SubnetType.Private }, + { name: 'Application2', subnetType: SubnetType.Private }, + { name: 'AnotherApplication', subnetType: SubnetType.Private }, + ], + }, + { + cidrMask: 27, + name: 'Database', + subnetType: SubnetType.Isolated, + } + ], +}); +``` +Then the subnet allocations change as follows: + * PublicSubnet1: 10.0.0.0/26 + * PublicSubnet2: 10.0.0.64/26 + * PublicSubnet3: 10.0.2.128/26 + * Application1Subnet1: 10.0.1.0/24 + * Application1Subnet2: 10.0.2.0/24 + * Application1Subnet3: 10.0.3.0/24 + * Application2Subnet1: 10.0.4.0/24 + * Application2Subnet2: 10.0.5.0/24 + * Application2Subnet3: 10.0.6.0/24 + * AnotherApplicationSubnet1: 10.0.7.0/24 + * AnotherApplicationSubnet2: 10.0.8.0/24 + * AnotherApplicationSubnet3: 10.0.9.0/24 + * DatabaseSubnet1: 10.0.31.0/27 + * DatabaseSubnet2: 10.0.31.32/27 + * DatabaseSubnet3: 10.0.31.64/27 + +Note that the addition of a new subnet did not cause IP renumbering but reused +a reserved IP space within the subnet group. + Teams may also become cost conscious and be willing to trade availability for cost. For example, in your test environments perhaps you would like the same VPC as production, but instead of 3 NAT Gateways you would like only 1. This will From 343c2e84c69cfa209d17b53790c5fc1e99052c21 Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Tue, 26 Mar 2019 16:14:54 +0100 Subject: [PATCH 4/7] feat(VpcNetwork): reverting previous changes --- packages/@aws-cdk/aws-ec2/README.md | 105 ------------- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 130 +--------------- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 171 +++------------------ 3 files changed, 26 insertions(+), 380 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index cddc127e93bba..f5b310ddad0e7 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -135,111 +135,6 @@ availability zones will be the following: Any subnet configuration without a `cidrMask` will be counted up and allocated evenly across the remaining IP space. -There are situations where the IP space for a group of subnets having the same -cidrMask's will need to be reserved. This is useful in situations where subnets -would need to be added after the vpc is originally deployed, without causing -IP renumbering for existing subnets. This configuration can be achieved by -defining the subnet group using SubnetGroupConfiguration. - -```ts -import ec2 = require('@aws-cdk/aws-ec2'); - -const vpc = new ec2.VpcNetwork(this, 'TheVPC', { - cidr: '10.0.0.0/16', - natGateways: 1, - subnetConfiguration: [ - { - cidrMask: 26, - name: 'Public', - subnetType: SubnetType.Public, - }, - { - subnetGroupName: 'Applications', - cidrMask: 24, - maxSubnets: 10, - subnetConfiguration: [ - { name: 'Application1', subnetType: SubnetType.Private }, - { name: 'Application2', subnetType: SubnetType.Private }, - ], - }, - { - cidrMask: 27, - name: 'Database', - subnetType: SubnetType.Isolated, - } - ], -}); -``` - -The `VpcNetwork` from the above configuration in a Region with three -availability zones will be the following: - * PublicSubnet1: 10.0.0.0/26 - * PublicSubnet2: 10.0.0.64/26 - * PublicSubnet3: 10.0.2.128/26 - * Application1Subnet1: 10.0.1.0/24 - * Application1Subnet2: 10.0.2.0/24 - * Application1Subnet3: 10.0.3.0/24 - * Application2Subnet1: 10.0.4.0/24 - * Application2Subnet2: 10.0.5.0/24 - * Application2Subnet3: 10.0.6.0/24 - * DatabaseSubnet1: 10.0.31.0/27 - * DatabaseSubnet2: 10.0.31.32/27 - * DatabaseSubnet3: 10.0.31.64/27 - -Note that in the above, the space 10.0.1.0/24 to 10.0.30.0/24 is all reserved -for the 'Applications' subnet group. At this point, if another application is -added as follows: - -```ts -import ec2 = require('@aws-cdk/aws-ec2'); - -const vpc = new ec2.VpcNetwork(this, 'TheVPC', { - cidr: '10.0.0.0/16', - natGateways: 1, - subnetConfiguration: [ - { - cidrMask: 26, - name: 'Public', - subnetType: SubnetType.Public, - }, - { - subnetGroupName: 'Applications', - cidrMask: 24, - maxSubnets: 10, - subnetConfiguration: [ - { name: 'Application1', subnetType: SubnetType.Private }, - { name: 'Application2', subnetType: SubnetType.Private }, - { name: 'AnotherApplication', subnetType: SubnetType.Private }, - ], - }, - { - cidrMask: 27, - name: 'Database', - subnetType: SubnetType.Isolated, - } - ], -}); -``` -Then the subnet allocations change as follows: - * PublicSubnet1: 10.0.0.0/26 - * PublicSubnet2: 10.0.0.64/26 - * PublicSubnet3: 10.0.2.128/26 - * Application1Subnet1: 10.0.1.0/24 - * Application1Subnet2: 10.0.2.0/24 - * Application1Subnet3: 10.0.3.0/24 - * Application2Subnet1: 10.0.4.0/24 - * Application2Subnet2: 10.0.5.0/24 - * Application2Subnet3: 10.0.6.0/24 - * AnotherApplicationSubnet1: 10.0.7.0/24 - * AnotherApplicationSubnet2: 10.0.8.0/24 - * AnotherApplicationSubnet3: 10.0.9.0/24 - * DatabaseSubnet1: 10.0.31.0/27 - * DatabaseSubnet2: 10.0.31.32/27 - * DatabaseSubnet3: 10.0.31.64/27 - -Note that the addition of a new subnet did not cause IP renumbering but reused -a reserved IP space within the subnet group. - Teams may also become cost conscious and be willing to trade availability for cost. For example, in your test environments perhaps you would like the same VPC as production, but instead of 3 NAT Gateways you would like only 1. This will diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 5c0386bf111f2..9465fdcf6dfc7 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -3,7 +3,7 @@ import { ConcreteDependable, IDependable } from '@aws-cdk/cdk'; 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 { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util'; import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider'; import { IVpcNetwork, IVpcSubnet, SubnetSelection, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcSubnetImportProps } from './vpc-ref'; import { VpnConnectionOptions, VpnConnectionType } from './vpn'; @@ -115,7 +115,7 @@ export interface VpcNetworkProps { * @default the VPC CIDR will be evenly divided between 1 public and 1 * private subnet per AZ */ - subnetConfiguration?: Array; + subnetConfiguration?: SubnetConfiguration[]; /** * Indicates whether a VPN gateway should be created and attached to this VPC. @@ -165,13 +165,6 @@ export enum DefaultInstanceTenancy { * Specify configuration parameters for a VPC to be built */ export interface SubnetConfiguration { - /** - * The name of the subnetGroup to which this subnet belongs to - * - * The corresponding subnet will be tagged with this name - */ - subnetGroup?: string; - /** * The CIDR Mask or the number of leading 1 bits in the routing mask * @@ -190,63 +183,12 @@ export interface SubnetConfiguration { /** * The common Logical Name for the `VpcSubnet` * - * This name will be suffixed with an integer correlating to a specific + * Thi name will be suffixed with an integer correlating to a specific * availability zone. */ name: string; } -/** - * Specify configuration parameters for a VPC to be built - */ -export interface SubnetGroupConfiguration { - /** - * The name of the subnetGroup - * - * All subnets withing this group will be tagged with this name - */ - subnetGroupName: string; - - /** - * The CIDR Mask or the number of leading 1 bits in the routing mask - * - * Valid values are 16 - 28 - */ - cidrMask: number; - - /** - * Configure the subnets to build for each AZ - * - * The subnets are constructed in the context of the VPC and SubnetGroup so you only need - * specify the configuration. The VPC details (VPC ID, specific CIDR, - * specific AZ will be calculated during creation) - * - * For example if you want 3 private subnets in the SubnetGroup - * in each AZ provide the following: - * subnetConfiguration: [ - * { - * name: 'applicationA', - * subnetType: SubnetType.Public, - * }, - * { - * name: 'applicationB', - * subnetType: SubnetType.Private, - * }, - * { - * name: 'applicationC', - * subnetType: SubnetType.Private, - * } - * ] - * - */ - subnetConfiguration?: SubnetConfiguration[]; - - /** - * The maximum number of subnets that can be in this subnet group - */ - maxSubnets: number; -} - /** * VpcNetwork deploys an AWS VPC, with public and private subnets per Availability Zone. * For example: @@ -401,14 +343,7 @@ export class VpcNetwork extends VpcNetworkBase { this.vpcId = this.resource.vpcId; - // Expand any subnetGroup to subnets - if (props.subnetConfiguration) { - props.subnetConfiguration = new Array().concat( - ...props.subnetConfiguration.map(this.subnetGroupToSubnets) - ); - } - - this.subnetConfiguration = ifUndefined(props.subnetConfiguration as SubnetConfiguration[], VpcNetwork.DEFAULT_SUBNETS); + this.subnetConfiguration = ifUndefined(props.subnetConfiguration, VpcNetwork.DEFAULT_SUBNETS); // subnetConfiguration and natGateways must be set before calling createSubnets this.createSubnets(); @@ -527,7 +462,7 @@ export class VpcNetwork extends VpcNetworkBase { } natSubnets = subnets as VpcPublicSubnet[]; } else { - natSubnets = this.publicSubnets as VpcPublicSubnet[]; + natSubnets = this.publicSubnets as VpcPublicSubnet[]; } natSubnets = natSubnets.slice(0, natCount); @@ -538,44 +473,6 @@ export class VpcNetwork extends VpcNetworkBase { } } - /** - * subnetGroupToSubnets expands a SubnetGroupConfiguration into an array of - * SubnetConfiguration. It appends any stub subnets at the end to ensure that the - * subnet ip space is pre allocated. If argument is a SubnetConfiguration, this - * is returned inside a single value array but is untouched - */ - private subnetGroupToSubnets(subnetOrSubnetGroup: SubnetConfiguration | SubnetGroupConfiguration): SubnetConfiguration[] { - if (!(subnetOrSubnetGroup as SubnetGroupConfiguration).subnetGroupName) { - // Argument is not a subnetGroup, return as is inside an array - return [subnetOrSubnetGroup as SubnetConfiguration]; - } - - const subnetGroup: SubnetGroupConfiguration = subnetOrSubnetGroup as SubnetGroupConfiguration; - subnetGroup.subnetConfiguration = subnetGroup.subnetConfiguration !== undefined ? subnetGroup.subnetConfiguration : []; - - // convert subnets in subnetGroup to an array of subnets - let result = subnetGroup.subnetConfiguration.map(subnet => { - return { - ...subnet, - ...objectIfDefined(subnetGroup.cidrMask, { cidrMask: subnetGroup.cidrMask }), - subnetGroup: subnetGroup.subnetGroupName, - }; - }); - - if (subnetGroup.maxSubnets) { - const remainingGroupSubnets = subnetGroup.maxSubnets - result.length; - if (remainingGroupSubnets < 0) { - throw Error(`Number of segments in group ${subnetGroup.subnetGroupName} is greater than defined maximum`); - } else { - const filler = { - ...objectIfDefined(subnetGroup.cidrMask, { cidrMask: subnetGroup.cidrMask }), - }; - result = result.concat(Array(remainingGroupSubnets).fill(filler)); - } - } - return result; - } - /** * createSubnets creates the subnets specified by the subnet configuration * array or creates the `DEFAULT_SUBNETS` configuration @@ -604,11 +501,6 @@ export class VpcNetwork extends VpcNetworkBase { private createSubnetResources(subnetConfig: SubnetConfiguration, cidrMask: number) { this.availabilityZones.forEach((zone, index) => { - if (!subnetConfig.name) { - // This is a filler subnet of a SubnetGroup - just reserve ip space and return - this.networkBuilder.addSubnet(cidrMask); - return; - } const name = subnetId(subnetConfig.name, index); const subnetProps: VpcSubnetProps = { availabilityZone: zone, @@ -640,18 +532,14 @@ export class VpcNetwork extends VpcNetworkBase { // These values will be used to recover the config upon provider import const includeResourceTypes = [CfnSubnet.resourceTypeName]; - subnet.node.apply(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, { includeResourceTypes })); - subnet.node.apply(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), { includeResourceTypes })); - if (subnetConfig.subnetGroup) { - subnet.node.apply(new cdk.Tag(SUBNETGROUP_TAG, subnetConfig.subnetGroup, { includeResourceTypes })); - } + subnet.node.apply(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, {includeResourceTypes})); + subnet.node.apply(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), {includeResourceTypes})); }); } } const SUBNETTYPE_TAG = 'aws-cdk:subnet-type'; const SUBNETNAME_TAG = 'aws-cdk:subnet-name'; -const SUBNETGROUP_TAG = 'aws-cdk:subnet-group'; function subnetTypeTagValue(type: SubnetType) { switch (type) { @@ -839,10 +727,6 @@ function ifUndefined(value: T | undefined, defaultValue: T): T { return value !== undefined ? value : defaultValue; } -function objectIfDefined(element: T | undefined, defaultValue: object): object { - return element !== undefined ? defaultValue : {}; -} - class ImportedVpcNetwork extends VpcNetworkBase { public readonly vpcId: string; public readonly publicSubnets: IVpcSubnet[]; diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 737534d867e30..0a7dc1e4f1607 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -10,7 +10,7 @@ export = { "vpc.vpcId returns a token to the VPC ID"(test: Test) { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - test.deepEqual(stack.node.resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); + test.deepEqual(stack.node.resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); test.done(); }, @@ -30,11 +30,11 @@ export = { new VpcNetwork(stack, 'TheVPC'); expect(stack).to( haveResource('AWS::EC2::VPC', - hasTags([{ Key: 'Name', Value: 'TheVPC' }])) + hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) ); expect(stack).to( haveResource('AWS::EC2::InternetGateway', - hasTags([{ Key: 'Name', Value: 'TheVPC' }])) + hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) ); test.done(); }, @@ -109,146 +109,13 @@ export = { "with no subnets defined, the VPC should have an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; - new VpcNetwork(stack, 'TheVPC', {}); + new VpcNetwork(stack, 'TheVPC', { }); expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); test.done(); }, - "with subnetGroups containing more subnets than maximum should throw an Error"(test: Test) { - const stack = getTestStack(); - test.throws(() => new VpcNetwork(stack, 'TheVPC', { - cidr: '10.0.0.0/16', - subnetConfiguration: [ - { - subnetGroupName: 'applicationsGroup', - cidrMask: 24, - maxSubnets: 3, - subnetConfiguration: [ - { name: 'application1', subnetType: SubnetType.Private }, - { name: 'application2', subnetType: SubnetType.Private }, - { name: 'application3', subnetType: SubnetType.Private }, - { name: 'application4', subnetType: SubnetType.Private }, - ], - }], - })); - test.done(); - }, - - "with custom subnets and subnetGroups, the VPC should contain correct number of subnets, an IGW, and a NAT Gateway per AZ "(test: Test) { - const stack = getTestStack(); - const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; - new VpcNetwork(stack, 'TheVPC', { - cidr: '10.0.0.0/16', - subnetConfiguration: [ - { - subnetGroupName: 'applicationsGroup', - cidrMask: 24, - maxSubnets: 4, - subnetConfiguration: [ - { name: 'application1', subnetType: SubnetType.Private }, - { name: 'application2', subnetType: SubnetType.Private }, - ], - }, - { - cidrMask: 24, - name: 'ingress', - subnetType: SubnetType.Public, - }, - { - cidrMask: 28, - name: 'rds', - subnetType: SubnetType.Isolated, - } - ], - maxAZs: 3 - }); - expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); - expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); - expect(stack).to(countResources("AWS::EC2::Subnet", 12)); - - test.done(); - }, - - "with custom subnets and subnetGroups, the VPC subnets should have correct CidrBlocks"(test: Test) { - const stack = getTestStack(); - new VpcNetwork(stack, 'TheVPC', { - cidr: '10.0.0.0/16', - subnetConfiguration: [ - { - subnetGroupName: 'applicationsGroup', - cidrMask: 24, - maxSubnets: 10, - subnetConfiguration: [ - { name: 'application1', subnetType: SubnetType.Private }, - { name: 'application2', subnetType: SubnetType.Private }, - ], - }, - { - cidrMask: 24, - name: 'ingress', - subnetType: SubnetType.Public, - }, - ], - maxAZs: 3 - }); - - // Subnets for application1 and applicaton2 - for (let i = 0; i < 6; i++) { - expect(stack).to(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.${i}.0/24` - })); - } - - // IP's within blocks 10.0.0.0/24 to 10.0.29.0/24 should be allocated by Subnet Group - - // Subnets for ingress - for (let i = 30; i < 33; i++) { - expect(stack).to(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.${i}.0/24` - })); - } - - test.done(); - }, - - "with subnetGroups without subnetConfiguration, no subnet should have cidrMask within reserved block"(test: Test) { - const stack = getTestStack(); - new VpcNetwork(stack, 'TheVPC', { - cidr: '10.0.0.0/16', - subnetConfiguration: [ - { - subnetGroupName: 'applicationsGroup', - cidrMask: 24, - maxSubnets: 10, - }, - { - cidrMask: 24, - name: 'ingress', - subnetType: SubnetType.Public, - }, - ], - maxAZs: 3 - }); - - // No Subnets for application group - for (let i = 0; i < 9; i++) { - expect(stack).notTo(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.${i}.0/24` - })); - } - - // Subnets for ingress - for (let i = 30; i < 33; i++) { - expect(stack).to(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.${i}.0/24` - })); - } - - test.done(); - }, - - "with custom subnets, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { + "with custom subents, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; new VpcNetwork(stack, 'TheVPC', { @@ -366,7 +233,7 @@ export = { } expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: {}, + NatGatewayId: { }, })); test.done(); @@ -384,7 +251,7 @@ export = { } expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: {}, + NatGatewayId: { }, })); test.done(); }, @@ -398,7 +265,7 @@ export = { expect(stack).to(countResources("AWS::EC2::NatGateway", 1)); expect(stack).to(haveResourceLike("AWS::EC2::Route", { DestinationCidrBlock: '0.0.0.0/0', - NatGatewayId: {}, + NatGatewayId: { }, })); test.done(); }, @@ -611,17 +478,17 @@ export = { 'When tagging': { 'VPC propagated tags will be on subnet, IGW, routetables, NATGW'(test: Test) { const stack = getTestStack(); - const tags = { + const tags = { VpcType: 'Good', }; const noPropTags = { BusinessUnit: 'Marketing', }; - const allTags = { ...tags, ...noPropTags }; + const allTags = {...tags, ...noPropTags}; const vpc = new VpcNetwork(stack, 'TheVPC'); // overwrite to set propagate - vpc.node.apply(new Tag('BusinessUnit', 'Marketing', { includeResourceTypes: [CfnVPC.resourceTypeName] })); + vpc.node.apply(new Tag('BusinessUnit', 'Marketing', {includeResourceTypes: [CfnVPC.resourceTypeName]})); vpc.node.apply(new Tag('VpcType', 'Good')); expect(stack).to(haveResource("AWS::EC2::VPC", hasTags(toCfnTags(allTags)))); const taggables = ['Subnet', 'InternetGateway', 'NatGateway', 'RouteTable']; @@ -637,12 +504,12 @@ export = { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); for (const subnet of vpc.publicSubnets) { - const tag = { Key: 'Name', Value: subnet.node.path }; + const tag = {Key: 'Name', Value: subnet.node.path}; expect(stack).to(haveResource('AWS::EC2::NatGateway', hasTags([tag]))); expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); } for (const subnet of vpc.privateSubnets) { - const tag = { Key: 'Name', Value: subnet.node.path }; + const tag = {Key: 'Name', Value: subnet.node.path}; expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); } test.done(); @@ -651,7 +518,7 @@ export = { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - const tag = { Key: 'Late', Value: 'Adder' }; + const tag = {Key: 'Late', Value: 'Adder'}; expect(stack).notTo(haveResource('AWS::EC2::VPC', hasTags([tag]))); vpc.node.apply(new Tag(tag.Key, tag.Value)); expect(stack).to(haveResource('AWS::EC2::VPC', hasTags([tag]))); @@ -852,17 +719,17 @@ function doImportExportTest(constructFn: (scope: Construct) => VpcNetwork): IVpc return VpcNetwork.import(stack2, 'VPC2', vpc1.export()); } -function toCfnTags(tags: any): Array<{ Key: string, Value: string }> { - return Object.keys(tags).map(key => { - return { Key: key, Value: tags[key] }; +function toCfnTags(tags: any): Array<{Key: string, Value: string}> { + return Object.keys(tags).map( key => { + return {Key: key, Value: tags[key]}; }); } -function hasTags(expectedTags: Array<{ Key: string, Value: string }>): (props: any) => boolean { +function hasTags(expectedTags: Array<{Key: string, Value: string}>): (props: any) => boolean { return (props: any) => { try { const tags = props.Tags; - const actualTags = tags.filter((tag: { Key: string, Value: string }) => { + const actualTags = tags.filter( (tag: {Key: string, Value: string}) => { for (const expectedTag of expectedTags) { if (isSuperObject(expectedTag, tag)) { return true; From 0bc39f88abac47555ca54d1e3d0b7f3f6fc41437 Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Fri, 29 Mar 2019 14:11:19 +0100 Subject: [PATCH 5/7] feat(subnets): Added reserved subnet configuration property --- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 9465fdcf6dfc7..940149cfab5b5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -183,10 +183,22 @@ export interface SubnetConfiguration { /** * The common Logical Name for the `VpcSubnet` * - * Thi name will be suffixed with an integer correlating to a specific + * This name will be suffixed with an integer correlating to a specific * availability zone. */ name: string; + + /** + * Subnet is not to be created but IP space needs to be reserved. + * + * When true, the IP space for the subnet is reserved but no actual + * resources are provisioned. This space is only dependent on the + * number of availibility zones and on `cidrMask` - all other subnet + * properties are ignored. + * + * @default false + */ + reserved?: boolean; } /** @@ -501,6 +513,12 @@ export class VpcNetwork extends VpcNetworkBase { private createSubnetResources(subnetConfig: SubnetConfiguration, cidrMask: number) { this.availabilityZones.forEach((zone, index) => { + if (subnetConfig.reserved === true) { + // For reserved subnets, just allocate ip space but do not create any resources + this.networkBuilder.addSubnet(cidrMask); + return; + } + const name = subnetId(subnetConfig.name, index); const subnetProps: VpcSubnetProps = { availabilityZone: zone, From 02ba078daebc65610e6b9318f45567cb87fbe570 Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Fri, 29 Mar 2019 14:12:17 +0100 Subject: [PATCH 6/7] feat(subnets): Added two tests for reserved subnets --- packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 0a7dc1e4f1607..4e8ce9e304b65 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -115,6 +115,74 @@ export = { test.done(); }, + "with subnets and reserved subnets defined, VPC subnet count should not contain reserved subnets "(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + cidrMask: 24, + subnetType: SubnetType.Private, + name: 'Private', + }, + { + cidrMask: 24, + name: 'reserved', + subnetType: SubnetType.Private, + reserved: true, + }, + { + cidrMask: 28, + name: 'rds', + subnetType: SubnetType.Isolated, + } + ], + maxAZs: 3 + }); + expect(stack).to(countResources("AWS::EC2::Subnet", 6)); + test.done(); + }, + "with reserved subents, any other subnets should not have cidrBlock from within reserved space"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Private, + }, + { + cidrMask: 24, + name: 'reserved', + subnetType: SubnetType.Private, + reserved: true, + }, + { + cidrMask: 24, + name: 'rds', + subnetType: SubnetType.Private, + } + ], + maxAZs: 3 + }); + for (let i = 0; i < 3; i++) { + expect(stack).to(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + for (let i = 3; i < 6; i++) { + expect(stack).notTo(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + for (let i = 6; i < 9; i++) { + expect(stack).to(haveResource("AWS::EC2::Subnet", { + CidrBlock: `10.0.${i}.0/24` + })); + } + test.done(); + }, "with custom subents, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; From 855ada22dc2ac65a5197909f377eec53b06da0e7 Mon Sep 17 00:00:00 2001 From: Darren Demicoli Date: Fri, 29 Mar 2019 14:12:41 +0100 Subject: [PATCH 7/7] feat(subnets): add documentation for reserved subnets --- packages/@aws-cdk/aws-ec2/README.md | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index f5b310ddad0e7..0912890ad284b 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -172,6 +172,52 @@ The `VpcNetwork` above will have the exact same subnet definitions as listed above. However, this time the VPC will have only 1 NAT Gateway and all Application subnets will route to the NAT Gateway. +#### Reserving subnet IP space +There are situations where the IP space for a subnet or number of subnets + will need to be reserved. This is useful in situations where subnets +would need to be added after the vpc is originally deployed, without causing +IP renumbering for existing subnets. The IP space for a subnet may be reserved +by setting the `reserved` subnetConfiguration property to true, as shown below: + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); +const vpc = new ec2.VpcNetwork(this, 'TheVPC', { + cidr: '10.0.0.0/16', + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 26, + name: 'Public', + subnetType: SubnetType.Public, + }, + { + cidrMask: 26, + name: 'Application1', + subnetType: SubnetType.Private, + }, + { + cidrMask: 26, + name: 'Application2', + subnetType: SubnetType.Private, + reserved: true, + }, + { + cidrMask: 27, + name: 'Database', + subnetType: SubnetType.Isolated, + } + ], +}); +``` + +In the example above, the subnet for Application2 is not actually provisioned +but its IP space is still reserved. If in the future this subnet needs to be +provisioned, then the `reserved: true` property should be removed. Most +importantly, this action would not cause the Database subnet to get renumbered, +but rather the IP space that was previously reserved will be used for the +subnet provisioned for Application2. The `reserved` property also takes into +consideration the number of availability zones when reserving IP space. + #### Sharing VPCs between stacks If you are creating multiple `Stack`s inside the same CDK application, you