Skip to content

Commit

Permalink
feat(aws-ec2) Configure NAT to Subnet Placement
Browse files Browse the repository at this point in the history
 * BREAKING change to VPC configuration
 * closes #741
 * Enable NAT placement by subnet name
 * Change VpcNeworkProps.natGateways from number to
   { gatewayCount: number; subnetName: string }
  • Loading branch information
moofish32 committed Oct 9, 2018
1 parent 1561a4d commit bbbeb65
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 32 deletions.
87 changes: 59 additions & 28 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cdk = require('@aws-cdk/cdk');
import { cloudformation } from './ec2.generated';
import { NetworkBuilder } from './network-util';
import { DEFAULT_SUBNET_NAME, subnetId } from './util';
import { DEFAULT_SUBNET_NAME, subnetId, subnetName } from './util';
import { SubnetType, VpcNetworkRef, VpcSubnetRef } from './vpc-ref';

/**
Expand Down Expand Up @@ -61,16 +61,9 @@ export interface VpcNetworkProps {
maxAZs?: number;

/**
* Define the maximum number of NAT Gateways for this VPC
*
* 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.
* @default maxAZs
* Define the NAT Gateway Configuration for this VPC
*/
natGateways?: number;
natGateways?: NatGatewayConfiguration;

/**
* Configure the subnets to build for each AZ
Expand Down Expand Up @@ -123,6 +116,27 @@ export enum DefaultInstanceTenancy {
Dedicated = 'dedicated'
}

export interface NatGatewayConfiguration {
/**
* The number of NAT Gateways to create.
*
* 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
*/
gatewayCount?: number;

/**
* The names of the subnets that will have NAT Gateways
*
* The names of the corresponding subnets in `SubnetConfiguration` that will
* have a NAT Gateway. If a corresponding subnet name is not found this will
* throw an error. By default the first public subnets will receive NAT
* Gateways until the `gatewayCount` is reached.
*/
subnetName?: string;
}

/**
* Specify configuration parameters for a VPC to be built
*/
Expand Down Expand Up @@ -231,13 +245,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
*/
Expand Down Expand Up @@ -303,8 +310,6 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable {
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();
Expand All @@ -321,12 +326,33 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable {
internetGatewayId: igw.ref,
vpcId: this.resource.ref
});
this.dependencyElements.push(igw, att);

const natConfig = ifUndefined(props.natGateways, {
gatewayCount: undefined,
subnetName: undefined,
});
const natCount = ifUndefined(natConfig.gatewayCount,
useNatGateway ? this.availabilityZones.length : 0);
const natSubnet = natConfig.subnetName;

if (natSubnet !== undefined) {
const subnetNames = (this.publicSubnets as VpcPublicSubnet[]).map( subnet => {
return subnetName(subnet);
});
if (!subnetNames.includes(natSubnet)) {
throw new Error(`NatGatewayConfiguration contains subnet name ${natSubnet} which is not a Public Subnet in SubnetConfiguration`);
}
}

(this.publicSubnets as VpcPublicSubnet[]).forEach(publicSubnet => {
publicSubnet.addDefaultIGWRouteEntry(igw.ref);
const currentNatCount = Object.values(this.natGatewayByAZ).length;
if (addNatGatewy(publicSubnet, natSubnet, natCount, currentNatCount)) {
this.natGatewayByAZ[publicSubnet.availabilityZone] = publicSubnet.addNatGateway();
}
});

this.dependencyElements.push(igw, att);

(this.privateSubnets as VpcPrivateSubnet[]).forEach((privateSubnet, i) => {
let ngwId = this.natGatewayByAZ[privateSubnet.availabilityZone];
if (ngwId === undefined) {
Expand Down Expand Up @@ -386,12 +412,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:
Expand Down Expand Up @@ -561,5 +581,16 @@ export class VpcPrivateSubnet extends VpcSubnet {
}

function ifUndefined<T>(value: T | undefined, defaultValue: T): T {
return value !== undefined ? value : defaultValue;
return value !== undefined ? value : defaultValue;
}

function addNatGatewy(subnet: VpcPublicSubnet, natSubnet: string | undefined, maxNats: number, natsCreated: number): boolean {
const name = subnetName(subnet);
if (natSubnet !== undefined && natSubnet !== name) {
return false;
}
if (natsCreated >= maxNats) {
return false;
}
return true;
}
67 changes: 63 additions & 4 deletions packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export = {
const stack = getTestStack();
new VpcNetwork(stack, 'TheVPC', {
cidr: '10.0.0.0/21',
natGateways: 2,
natGateways: { gatewayCount: 2},
subnetConfiguration: [
{
cidrMask: 24,
Expand Down Expand Up @@ -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: { gatewayCount: 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,
},
],
natGateways: {
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,
},
],
natGateways: {
subnetName: 'notthere',
},
}));
test.done();
},

},

Expand Down

0 comments on commit bbbeb65

Please sign in to comment.