Skip to content

Commit

Permalink
fix(aws-ec2): VPC properly uses complex subnet config
Browse files Browse the repository at this point in the history
It is now possible to pick Isolated subnets for the VPC placement
strategy. Also, Isolated subnets are properly exported and imported
using the VPC export()/import() functions, and it's now possible
not have all three of public/private/isolated subnets in the
export/import.

Fixes #597.
  • Loading branch information
Rico Huijbers committed Aug 20, 2018
1 parent a9114c5 commit 319e0cc
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 66 deletions.
126 changes: 106 additions & 20 deletions packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
import { Construct, IDependable, Output, StringListOutput, Token } from "@aws-cdk/cdk";

/**
* The type of Subnet
*/
export enum SubnetType {
/**
* Isolated Subnets do not route Outbound traffic
*
* This can be good for subnets with RDS or
* Elasticache endpoints
*/
Isolated = 1,

/**
* Subnet that routes to the internet, but not vice versa.
*
* Instances in a private subnet can connect to the Internet, but will not
* allow connections to be initiated from the Internet.
*
* Outbound traffic will be routed via a NAT Gateway. Preference being in
* the same AZ, but if not available will use another AZ. This is common for
* experimental cost conscious accounts or accounts where HA outbound
* traffic is not needed.
*/
Private = 2,

/**
* Subnet connected to the Internet
*
* Instances in a Public subnet can connect to the Internet and can be
* connected to from the Internet as long as they are launched with public IPs.
*
* Public subnets route outbound traffic via an Internet Gateway.
*/
Public = 3
}

/**
* Customize how instances are placed inside a VPC
*
Expand All @@ -8,13 +44,13 @@ import { Construct, IDependable, Output, StringListOutput, Token } from "@aws-cd
*/
export interface VpcPlacementStrategy {
/**
* Whether to use the VPC's public subnets to start instances
* What subnet type to place the instances in
*
* If false, the instances are started in the private subnets.
* By default, the instances are placed in the private subnets.
*
* @default false
* @default SubnetType.Private
*/
usePublicSubnets?: boolean;
subnetsToUse?: SubnetType;
}

/**
Expand Down Expand Up @@ -43,6 +79,16 @@ export abstract class VpcNetworkRef extends Construct implements IDependable {
*/
public abstract readonly privateSubnets: VpcSubnetRef[];

/**
* List of isolated subnets in this VPC
*/
public abstract readonly isolatedSubnets: VpcSubnetRef[];

/**
* AZs for this VPC
*/
public abstract readonly availabilityZones: string[];

/**
* Parts of the VPC that constitute full construction
*/
Expand All @@ -51,9 +97,13 @@ export abstract class VpcNetworkRef extends Construct implements IDependable {
/**
* Return the subnets appropriate for the placement strategy
*/
public subnets(placement?: VpcPlacementStrategy): VpcSubnetRef[] {
if (!placement) { return this.privateSubnets; }
return placement.usePublicSubnets ? this.publicSubnets : this.privateSubnets;
public subnets(placement: VpcPlacementStrategy = {}): VpcSubnetRef[] {
if (placement.subnetsToUse === undefined) { return this.privateSubnets; }
return {
[SubnetType.Isolated]: this.isolatedSubnets,
[SubnetType.Private]: this.privateSubnets,
[SubnetType.Public]: this.publicSubnets,
}[placement.subnetsToUse];
}

/**
Expand All @@ -62,11 +112,17 @@ export abstract class VpcNetworkRef extends Construct implements IDependable {
public export(): VpcNetworkRefProps {
return {
vpcId: new Output(this, 'VpcId', { value: this.vpcId }).makeImportValue(),
availabilityZones: this.publicSubnets.map(s => s.availabilityZone),
publicSubnetIds: new StringListOutput(this, 'PublicSubnetIDs', { values: this.publicSubnets.map(s => s.subnetId) }).makeImportValues(),
privateSubnetIds: new StringListOutput(this, 'PrivateSubnetIDs', { values: this.privateSubnets.map(s => s.subnetId) }).makeImportValues(),
availabilityZones: this.availabilityZones,
publicSubnetIds: this.exportSubnetIds('PublicSubnetIDs', this.publicSubnets),
privateSubnetIds: this.exportSubnetIds('PrivateSubnetIDs', this.privateSubnets),
isolatedSubnetIds: this.exportSubnetIds('IsolatedSubnetIDs', this.isolatedSubnets),
};
}

private exportSubnetIds(name: string, subnets: VpcSubnetRef[]): Token[] | undefined {
if (subnets.length === 0) { return undefined; }
return new StringListOutput(this, name, { values: subnets.map(s => s.subnetId) }).makeImportValues();
}
}

/**
Expand All @@ -88,27 +144,50 @@ class ImportedVpcNetwork extends VpcNetworkRef {
*/
public readonly privateSubnets: VpcSubnetRef[];

/**
* List of isolated subnets in this VPC
*/
public readonly isolatedSubnets: VpcSubnetRef[];

/**
* AZs for this VPC
*/
public readonly availabilityZones: string[];

constructor(parent: Construct, name: string, props: VpcNetworkRefProps) {
super(parent, name);

this.vpcId = props.vpcId;
this.availabilityZones = props.availabilityZones;

const privateSubnetIds = props.privateSubnetIds || [];
const publicSubnetIds = props.publicSubnetIds || [];
const isolatedSubnetIds = props.isolatedSubnetIds || [];

if (props.availabilityZones.length !== props.publicSubnetIds.length) {
throw new Error('Availability zone and public subnet ID arrays must be same length');
if (publicSubnetIds.length > 0 && this.availabilityZones.length !== publicSubnetIds.length) {
throw new Error('Must have Public subnet for every AZ');
}

if (props.availabilityZones.length !== props.privateSubnetIds.length) {
throw new Error('Availability zone and private subnet ID arrays must be same length');
if (privateSubnetIds.length > 0 && this.availabilityZones.length !== privateSubnetIds.length) {
throw new Error('Must have Private subnet for every AZ');
}

if (isolatedSubnetIds.length > 0 && this.availabilityZones.length !== isolatedSubnetIds.length) {
throw new Error('Must have Isolated subnet for every AZ');
}

const n = props.availabilityZones.length;
this.publicSubnets = range(n).map(i => VpcSubnetRef.import(this, `PublicSubnet${i}`, {
availabilityZone: props.availabilityZones[i],
subnetId: props.publicSubnetIds[i]
availabilityZone: this.availabilityZones[i],
subnetId: publicSubnetIds[i]
}));
this.privateSubnets = range(n).map(i => VpcSubnetRef.import(this, `PrivateSubnet${i}`, {
availabilityZone: props.availabilityZones[i],
subnetId: props.privateSubnetIds[i]
availabilityZone: this.availabilityZones[i],
subnetId: privateSubnetIds[i]
}));
this.isolatedSubnets = range(n).map(i => VpcSubnetRef.import(this, `IsolatedSubnet${i}`, {
availabilityZone: this.availabilityZones[i],
subnetId: isolatedSubnetIds[i]
}));
}
}
Expand All @@ -135,14 +214,21 @@ export interface VpcNetworkRefProps {
*
* Must match the availability zones and private subnet ids in length and order.
*/
publicSubnetIds: VpcSubnetId[];
publicSubnetIds?: VpcSubnetId[];

/**
* List of private subnet IDs, one for every subnet
*
* Must match the availability zones and public subnet ids in length and order.
*/
privateSubnetIds: VpcSubnetId[];
privateSubnetIds?: VpcSubnetId[];

/**
* List of isolated subnet IDs, one for every subnet
*
* Must match the availability zones and public subnet ids in length and order.
*/
isolatedSubnetIds?: VpcSubnetId[];
}

/**
Expand Down
52 changes: 6 additions & 46 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import cdk = require('@aws-cdk/cdk');
import { Obj } from '@aws-cdk/util';
import { cloudformation } from './ec2.generated';
import { NetworkBuilder } from './network-util';
import { VpcNetworkId, VpcNetworkRef, VpcSubnetId, VpcSubnetRef } from './vpc-ref';
import { SubnetType, VpcNetworkId, VpcNetworkRef, VpcSubnetId, VpcSubnetRef } from './vpc-ref';
/**
* VpcNetworkProps allows you to specify configuration options for a VPC
*/
Expand Down Expand Up @@ -117,44 +117,6 @@ export enum DefaultInstanceTenancy {
Dedicated = 'dedicated'
}

/**
* The type of Subnet
*/
export enum SubnetType {

/**
* Isolated Subnets do not route Outbound traffic
*
* This can be good for subnets with RDS or
* Elasticache endpoints
*/
Isolated = 1,

/**
* Subnet that routes to the internet, but not vice versa.
*
* Instances in a private subnet can connect to the Internet, but will not
* allow connections to be initiated from the Internet.
*
* Outbound traffic will be routed via a NAT Gateway. Preference being in
* the same AZ, but if not available will use another AZ. This is common for
* experimental cost conscious accounts or accounts where HA outbound
* traffic is not needed.
*/
Private = 2,

/**
* Subnet connected to the Internet
*
* Instances in a Public subnet can connect to the Internet and can be
* connected to from the Internet as long as they are launched with public IPs.
*
* Public subnets route outbound traffic via an Internet Gateway.
*/
Public = 3

}

/**
* Specify configuration parameters for a VPC to be built
*/
Expand Down Expand Up @@ -248,6 +210,11 @@ export class VpcNetwork extends VpcNetworkRef {
*/
public readonly isolatedSubnets: VpcSubnetRef[] = [];

/**
* AZs for this VPC
*/
public readonly availabilityZones: string[];

/**
* Maximum Number of NAT Gateways used to control cost
*
Expand Down Expand Up @@ -275,13 +242,6 @@ export class VpcNetwork extends VpcNetworkRef {
*/
private subnetConfiguration: SubnetConfiguration[] = [];

/**
* Maximum AZs to Uses for this VPC
*
* @default All
*/
private availabilityZones: string[];

/**
* VpcNetwork creates a VPC that spans a whole region.
* It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone.
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/test/test.fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,5 +241,6 @@ function mockVpc(stack: Stack) {
availabilityZones: [ 'az1' ],
publicSubnetIds: [ new VpcSubnetId('pub1') ],
privateSubnetIds: [ new VpcSubnetId('pri1') ],
isolatedSubnetIds: [],
});
}
55 changes: 55 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,62 @@ export = {
}));
test.done();
}
},

'can select public subnets'(test: Test) {
// GIVEN
const stack = getTestStack();
const vpc = new VpcNetwork(stack, 'VPC');

// WHEN
const nets = vpc.subnets({ subnetsToUse: SubnetType.Public });

// THEN
test.deepEqual(nets, vpc.publicSubnets);

test.done();
},

'can select isolated subnets'(test: Test) {
// GIVEN
const stack = getTestStack();
const vpc = new VpcNetwork(stack, 'VPC', {
subnetConfiguration: [
{ subnetType: SubnetType.Private, name: 'Private' },
{ subnetType: SubnetType.Isolated, name: 'Isolated' },
]
});

// WHEN
const nets = vpc.subnets({ subnetsToUse: SubnetType.Isolated });

// THEN
test.deepEqual(nets, vpc.isolatedSubnets);

test.done();
},

'can select isolated subnets after exporting and importing'(test: Test) {
// GIVEN
const stack1 = getTestStack();
const stack2 = getTestStack();
const vpc1 = new VpcNetwork(stack1, 'VPC', {
subnetConfiguration: [
{ subnetType: SubnetType.Private, name: 'Private' },
{ subnetType: SubnetType.Isolated, name: 'Isolated' },
]
});

const importedVpc = VpcNetwork.import(stack2, 'VPC', vpc1.export());

// WHEN
const nets = importedVpc.subnets({ subnetsToUse: SubnetType.Isolated });

// THEN
test.equal(3, importedVpc.isolatedSubnets.length);
test.deepEqual(nets, importedVpc.isolatedSubnets);

test.done();
},
};

Expand Down

0 comments on commit 319e0cc

Please sign in to comment.