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

fix(aws-ec2): VPC properly uses complex subnet config #610

Merged
merged 10 commits into from
Sep 9, 2018
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add information in the the last paragraph that explains how to achieve this setup.

* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reference on how to launch an instance with a public IP

*
* Public subnets route outbound traffic via an Internet Gateway.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... in the same AZ?

*/
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our VPCs commonly have an egress PublicSubnet and ingress PublicSubnet (per AZ). The separation is so we can place NACL on the egress and get some defense in depth plus avoid requiring an outbound layer 7 proxy for all situations. In order to use this we somehow need to place our bastion ASG in the ingress PublicSubnet, but right now we have no way to do that. One option is to allow an optional field called subnetIds as an escape back to what we know from traditional CloudFormation. Another option would be to enable some sort of selection based on tags (which isn't ready yet via #538).

Any opposition to adding a subnets: VpcSubnetRef[] or subnetIds: string[]. I think the former is more safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel I would prefer tags or something similar. It doesn't have to be done all at once, we can add support for tag selection after that PR has landed.

In the mean time, we can also allow selecting by name, since we tagged them with names in the config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on names or anything that gives us the ability to select to specific subnet when type is not distinct

}

/**
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
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const cluster = new DatabaseCluster(stack, 'Database', {
instanceProps: {
instanceType: new InstanceTypePair(InstanceClass.Burstable2, InstanceSize.Small),
vpcPlacement: {
usePublicSubnets: true
subnetsToUse: ec2.SubnetType.Public,
},
vpc
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class DatabaseCluster extends DatabaseClusterRef {
props.clusterIdentifier != null ? `${props.clusterIdentifier}instance${instanceIndex}` :
undefined;

const publiclyAccessible = props.instanceProps.vpcPlacement && props.instanceProps.vpcPlacement.usePublicSubnets;
const publiclyAccessible = props.instanceProps.vpcPlacement && props.instanceProps.vpcPlacement.subnetsToUse === ec2.SubnetType.Public;

const instance = new cloudformation.DBInstanceResource(this, `Instance${instanceIndex}`, {
// Link to cluster
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-rds/test/integ.cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const cluster = new DatabaseCluster(stack, 'Database', {
},
instanceProps: {
instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small),
vpcPlacement: { usePublicSubnets: true },
vpcPlacement: { subnetsToUse: ec2.SubnetType.Public },
vpc
}
});
Expand Down