Skip to content

Commit

Permalink
feat(aws-ec2): allow configuring subnets for NAT gateway (#874)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
moofish32 authored and rix0rrr committed Oct 10, 2018
1 parent 66ff0a8 commit 4a76287
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 31 deletions.
74 changes: 46 additions & 28 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();

Expand All @@ -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];
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -561,5 +579,5 @@ export class VpcPrivateSubnet extends VpcSubnet {
}

function ifUndefined<T>(value: T | undefined, defaultValue: T): T {
return value !== undefined ? value : defaultValue;
return value !== undefined ? value : defaultValue;
}
65 changes: 62 additions & 3 deletions packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
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: 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();
},

},

Expand Down

0 comments on commit 4a76287

Please sign in to comment.