Skip to content

Commit 0de2206

Browse files
authored
fix(ec2): descriptive error message when selecting 0 subnets (#2025)
Selecting 0 subnets is invalid for most CFN resources. We move the check for this upstream to the VPC construct, which can throw a nicely descriptive error message if it happens to not contain any subnets that match the type we're looking for. The alternative is that the error happens during CloudFormation deployment, but then it's not very clear anymore what the cause for the error was. Fixes #2011. BREAKING CHANGE: `vpcPlacement` has been renamed to `vpcSubnets` on all objects, `subnetsToUse` has been renamed to `subnetType`. `natGatewayPlacement` has been renamed to `natGatewaySubnets`.
1 parent 86e9d03 commit 0de2206

File tree

20 files changed

+270
-141
lines changed

20 files changed

+270
-141
lines changed

packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface CommonAutoScalingGroupProps {
5353
/**
5454
* Where to place instances within the VPC
5555
*/
56-
vpcPlacement?: ec2.VpcPlacementStrategy;
56+
vpcSubnets?: ec2.SubnetSelection;
5757

5858
/**
5959
* SNS topic to send notifications about fleet changes
@@ -291,8 +291,7 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup
291291
});
292292
}
293293

294-
const subnets = props.vpc.subnets(props.vpcPlacement);
295-
asgProps.vpcZoneIdentifier = subnets.map(n => n.subnetId);
294+
asgProps.vpcZoneIdentifier = props.vpc.subnetIds(props.vpcSubnets);
296295

297296
this.autoScalingGroup = new CfnAutoScalingGroup(this, 'ASG', asgProps);
298297
this.osType = machineImage.os.type;

packages/@aws-cdk/aws-ec2/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ instances for your project.
2121

2222
Our default `VpcNetwork` class creates a private and public subnet for every
2323
availability zone. Classes that use the VPC will generally launch instances
24-
into all private subnets, and provide a parameter called `vpcPlacement` to
24+
into all private subnets, and provide a parameter called `vpcSubnets` to
2525
allow you to override the placement. [Read more about
2626
subnets](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html).
2727

packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts

Lines changed: 116 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk";
2-
import { subnetName } from './util';
2+
import { DEFAULT_SUBNET_NAME, subnetName } from './util';
33
import { VpnConnection, VpnConnectionOptions } from './vpn';
44

55
export interface IVpcSubnet extends IConstruct {
@@ -61,9 +61,20 @@ export interface IVpcNetwork extends IConstruct {
6161
readonly vpnGatewayId?: string;
6262

6363
/**
64-
* Return the subnets appropriate for the placement strategy
64+
* Return IDs of the subnets appropriate for the given selection strategy
65+
*
66+
* Requires that at least once subnet is matched, throws a descriptive
67+
* error message otherwise.
68+
*
69+
* Prefer to use this method over {@link subnets} if you need to pass subnet
70+
* IDs to a CloudFormation Resource.
6571
*/
66-
subnets(placement?: VpcPlacementStrategy): IVpcSubnet[];
72+
subnetIds(selection?: SubnetSelection): string[];
73+
74+
/**
75+
* Return a dependable object representing internet connectivity for the given subnets
76+
*/
77+
subnetInternetDependencies(selection?: SubnetSelection): IDependable;
6778

6879
/**
6980
* Return whether the given subnet is one of this VPC's public subnets.
@@ -125,29 +136,29 @@ export enum SubnetType {
125136
}
126137

127138
/**
128-
* Customize how instances are placed inside a VPC
139+
* Customize subnets that are selected for placement of ENIs
129140
*
130141
* Constructs that allow customization of VPC placement use parameters of this
131142
* type to provide placement settings.
132143
*
133144
* By default, the instances are placed in the private subnets.
134145
*/
135-
export interface VpcPlacementStrategy {
146+
export interface SubnetSelection {
136147
/**
137148
* Place the instances in the subnets of the given type
138149
*
139-
* At most one of `subnetsToUse` and `subnetName` can be supplied.
150+
* At most one of `subnetType` and `subnetName` can be supplied.
140151
*
141152
* @default SubnetType.Private
142153
*/
143-
subnetsToUse?: SubnetType;
154+
subnetType?: SubnetType;
144155

145156
/**
146157
* Place the instances in the subnets with the given name
147158
*
148159
* (This is the name supplied in subnetConfiguration).
149160
*
150-
* At most one of `subnetsToUse` and `subnetName` can be supplied.
161+
* At most one of `subnetType` and `subnetName` can be supplied.
151162
*
152163
* @default name
153164
*/
@@ -200,31 +211,30 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
200211
public readonly natDependencies = new Array<IConstruct>();
201212

202213
/**
203-
* Return the subnets appropriate for the placement strategy
214+
* Returns IDs of selected subnets
204215
*/
205-
public subnets(placement: VpcPlacementStrategy = {}): IVpcSubnet[] {
206-
if (placement.subnetsToUse !== undefined && placement.subnetName !== undefined) {
207-
throw new Error('At most one of subnetsToUse and subnetName can be supplied');
208-
}
216+
public subnetIds(selection: SubnetSelection = {}): string[] {
217+
selection = reifySelectionDefaults(selection);
209218

210-
// Select by name
211-
if (placement.subnetName !== undefined) {
212-
const allSubnets = this.privateSubnets.concat(this.publicSubnets).concat(this.isolatedSubnets);
213-
const selectedSubnets = allSubnets.filter(s => subnetName(s) === placement.subnetName);
214-
if (selectedSubnets.length === 0) {
215-
throw new Error(`No subnets with name: ${placement.subnetName}`);
216-
}
217-
return selectedSubnets;
219+
const nets = this.subnets(selection);
220+
if (nets.length === 0) {
221+
throw new Error(`There are no ${describeSelection(selection)} in this VPC. Use a different VPC subnet selection.`);
218222
}
219223

220-
// Select by type
221-
if (placement.subnetsToUse === undefined) { return this.privateSubnets; }
224+
return nets.map(n => n.subnetId);
225+
}
222226

223-
return {
224-
[SubnetType.Isolated]: this.isolatedSubnets,
225-
[SubnetType.Private]: this.privateSubnets,
226-
[SubnetType.Public]: this.publicSubnets,
227-
}[placement.subnetsToUse];
227+
/**
228+
* Return a dependable object representing internet connectivity for the given subnets
229+
*/
230+
public subnetInternetDependencies(selection: SubnetSelection = {}): IDependable {
231+
selection = reifySelectionDefaults(selection);
232+
233+
const ret = new CompositeDependable();
234+
for (const subnet of this.subnets(selection)) {
235+
ret.add(subnet.internetConnectivityEstablished);
236+
}
237+
return ret;
228238
}
229239

230240
/**
@@ -260,6 +270,31 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
260270
return this.node.stack.region;
261271
}
262272

273+
/**
274+
* Return the subnets appropriate for the placement strategy
275+
*/
276+
protected subnets(selection: SubnetSelection = {}): IVpcSubnet[] {
277+
selection = reifySelectionDefaults(selection);
278+
279+
// Select by name
280+
if (selection.subnetName !== undefined) {
281+
const allSubnets = this.privateSubnets.concat(this.publicSubnets).concat(this.isolatedSubnets);
282+
const selectedSubnets = allSubnets.filter(s => subnetName(s) === selection.subnetName);
283+
if (selectedSubnets.length === 0) {
284+
throw new Error(`No subnets with name: ${selection.subnetName}`);
285+
}
286+
return selectedSubnets;
287+
}
288+
289+
// Select by type
290+
if (selection.subnetType === undefined) { return this.privateSubnets; }
291+
292+
return {
293+
[SubnetType.Isolated]: this.isolatedSubnets,
294+
[SubnetType.Private]: this.privateSubnets,
295+
[SubnetType.Public]: this.publicSubnets,
296+
}[selection.subnetType];
297+
}
263298
}
264299

265300
/**
@@ -335,3 +370,56 @@ export interface VpcSubnetImportProps {
335370
*/
336371
subnetId: string;
337372
}
373+
374+
/**
375+
* If the placement strategy is completely "default", reify the defaults so
376+
* consuming code doesn't have to reimplement the same analysis every time.
377+
*
378+
* Returns "private subnets" by default.
379+
*/
380+
function reifySelectionDefaults(placement: SubnetSelection): SubnetSelection {
381+
if (placement.subnetType !== undefined && placement.subnetName !== undefined) {
382+
throw new Error('Only one of subnetType and subnetName can be supplied');
383+
}
384+
385+
if (placement.subnetType === undefined && placement.subnetName === undefined) {
386+
return { subnetType: SubnetType.Private };
387+
}
388+
389+
return placement;
390+
}
391+
392+
/**
393+
* Describe the given placement strategy
394+
*/
395+
function describeSelection(placement: SubnetSelection): string {
396+
if (placement.subnetType !== undefined) {
397+
return `'${DEFAULT_SUBNET_NAME[placement.subnetType]}' subnets`;
398+
}
399+
if (placement.subnetName !== undefined) {
400+
return `subnets named '${placement.subnetName}'`;
401+
}
402+
return JSON.stringify(placement);
403+
}
404+
405+
class CompositeDependable implements IDependable {
406+
private readonly dependables = new Array<IDependable>();
407+
408+
/**
409+
* Add a construct to the dependency roots
410+
*/
411+
public add(dep: IDependable) {
412+
this.dependables.push(dep);
413+
}
414+
415+
/**
416+
* Retrieve the current set of dependency roots
417+
*/
418+
public get dependencyRoots(): IConstruct[] {
419+
const ret = [];
420+
for (const dep of this.dependables) {
421+
ret.push(...dep.dependencyRoots);
422+
}
423+
return ret;
424+
}
425+
}

packages/@aws-cdk/aws-ec2/lib/vpc.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVP
55
import { NetworkBuilder } from './network-util';
66
import { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util';
77
import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider';
8-
import { IVpcNetwork, IVpcSubnet, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcPlacementStrategy, VpcSubnetImportProps } from './vpc-ref';
8+
import { IVpcNetwork, IVpcSubnet, SubnetSelection, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcSubnetImportProps } from './vpc-ref';
99
import { VpnConnectionOptions, VpnConnectionType } from './vpn';
1010

1111
/**
@@ -80,7 +80,7 @@ export interface VpcNetworkProps {
8080
*
8181
* @default All public subnets
8282
*/
83-
natGatewayPlacement?: VpcPlacementStrategy;
83+
natGatewaySubnets?: SubnetSelection;
8484

8585
/**
8686
* Configure the subnets to build for each AZ
@@ -365,7 +365,7 @@ export class VpcNetwork extends VpcNetworkBase {
365365
});
366366

367367
// if gateways are needed create them
368-
this.createNatGateways(props.natGateways, props.natGatewayPlacement);
368+
this.createNatGateways(props.natGateways, props.natGatewaySubnets);
369369

370370
(this.privateSubnets as VpcPrivateSubnet[]).forEach((privateSubnet, i) => {
371371
let ngwId = this.natGatewayByAZ[privateSubnet.availabilityZone];
@@ -445,7 +445,7 @@ export class VpcNetwork extends VpcNetworkBase {
445445
};
446446
}
447447

448-
private createNatGateways(gateways?: number, placement?: VpcPlacementStrategy): void {
448+
private createNatGateways(gateways?: number, placement?: SubnetSelection): void {
449449
const useNatGateway = this.subnetConfiguration.filter(
450450
subnet => (subnet.subnetType === SubnetType.Private)).length > 0;
451451

packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ new ec2.SecurityGroup(stack, 'SecurityGroup', {
1818
});
1919

2020
// Try subnet selection
21-
new cdk.CfnOutput(stack, 'PublicSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Public }).map(s => s.subnetId).join(',') });
22-
new cdk.CfnOutput(stack, 'PrivateSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Private }).map(s => s.subnetId).join(',') });
21+
new cdk.CfnOutput(stack, 'PublicSubnets', { value: 'ids:' + vpc.publicSubnets.map(s => s.subnetId).join(',') });
22+
new cdk.CfnOutput(stack, 'PrivateSubnets', { value: 'ids:' + vpc.privateSubnets.map(s => s.subnetId).join(',') });
2323

2424
app.run();

0 commit comments

Comments
 (0)