Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(VpcNetwork): support reserved subnets in subnetConfiguration #2090

Merged
merged 8 commits into from
Apr 1, 2019
105 changes: 105 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 123 additions & 7 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<SubnetConfiguration | SubnetGroupConfiguration>;

/**
* Indicates whether a VPN gateway should be created and attached to this VPC.
Expand Down Expand Up @@ -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
*
Expand All @@ -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:
Expand Down Expand Up @@ -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<SubnetConfiguration>().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();

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -727,6 +839,10 @@ function ifUndefined<T>(value: T | undefined, defaultValue: T): T {
return value !== undefined ? value : defaultValue;
}

function objectIfDefined<T>(element: T | undefined, defaultValue: object): object {
return element !== undefined ? defaultValue : {};
}

class ImportedVpcNetwork extends VpcNetworkBase {
public readonly vpcId: string;
public readonly publicSubnets: IVpcSubnet[];
Expand Down
Loading