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(ec2): descriptive error message when selecting 0 subnets #2025

Merged
merged 4 commits into from
Mar 21, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,7 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup
});
}

const subnets = props.vpc.subnets(props.vpcPlacement);
asgProps.vpcZoneIdentifier = subnets.map(n => n.subnetId);
asgProps.vpcZoneIdentifier = props.vpc.subnetIds(props.vpcPlacement);

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 61 additions & 4 deletions packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk";
import { subnetName } from './util';
import { DEFAULT_SUBNET_NAME, subnetName } from './util';
import { VpnConnection, VpnConnectionOptions } from './vpn';

export interface IVpcSubnet extends IConstruct {
Expand Down Expand Up @@ -62,9 +62,23 @@ export interface IVpcNetwork extends IConstruct {

/**
* Return the subnets appropriate for the placement strategy
*
* Prefer using {@link subnetIds} if you need to pass subnet IDs to a
* CloudFormation Resource.
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
*/
subnets(placement?: VpcPlacementStrategy): IVpcSubnet[];

/**
* Return IDs of the subnets appropriate for the given placement strategy
*
* Requires that at least once subnet is matched, throws a descriptive
* error message otherwise.
*
* Prefer to use this method over {@link subnets} if you need to pass subnet
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
* IDs to a CloudFormation Resource.
*/
subnetIds(placement?: VpcPlacementStrategy): string[];

/**
* Return whether the given subnet is one of this VPC's public subnets.
*
Expand Down Expand Up @@ -203,9 +217,7 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
* Return the subnets appropriate for the placement strategy
*/
public subnets(placement: VpcPlacementStrategy = {}): IVpcSubnet[] {
if (placement.subnetsToUse !== undefined && placement.subnetName !== undefined) {
throw new Error('At most one of subnetsToUse and subnetName can be supplied');
}
placement = reifyDefaultsPlacement(placement);

// Select by name
if (placement.subnetName !== undefined) {
Expand All @@ -227,6 +239,20 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork {
}[placement.subnetsToUse];
}

/**
* Returns IDs of selected subnets
*/
public subnetIds(placement: VpcPlacementStrategy = {}): string[] {
placement = reifyDefaultsPlacement(placement);

const nets = this.subnets(placement);
if (nets.length === 0) {
throw new Error(`There are no ${describePlacement(placement)} in this VPC. Use a different VPC placement strategy.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we maybe hint at what strategies would have matched something? Or are placement strategies too complex?

Copy link
Contributor Author

@rix0rrr rix0rrr Mar 18, 2019

Choose a reason for hiding this comment

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

They are not necessarily, but what are you proposing?

"The VPC network contains 1 Public subnet group with name "Public" and 2 Private subnets groups with names "MyDatabases","MyLambdas"

?

Not sure that a longer error message is necessarily going to make things better. We could, of course.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean I wouldn't put the Use a different VPC placement strategy if you can't hint at which are "valid".

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 used that terminology on purpose because:

  • It's okay to state an error, but it's better to state a path to a solution.
  • I want to use the words "VPC placement" here on purpose, because previously asked questions have shown that people don't think of this term.

Although maybe the better solution is to change the parameter name to subnetSelection, or something, under the "s" where people that already know things about AWS and CFN are going to look for it.

}

return nets.map(n => n.subnetId);
}

/**
* Adds a new VPN connection to this VPC
*/
Expand Down Expand Up @@ -335,3 +361,34 @@ export interface VpcSubnetImportProps {
*/
subnetId: string;
}

/**
* If the placement strategy is completely "default", reify the defaults so
* consuming code doesn't have to reimplement the same analysis every time.
*
* Returns "private subnets" by default.
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
*/
function reifyDefaultsPlacement(placement: VpcPlacementStrategy): VpcPlacementStrategy {
if (placement.subnetsToUse !== undefined && placement.subnetName !== undefined) {
throw new Error('At most one of subnetsToUse and subnetName can be supplied');
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
}

if (placement.subnetsToUse === undefined && placement.subnetName === undefined) {
return { subnetsToUse: SubnetType.Private };
}

return placement;
}

/**
* Describe the given placement strategy
*/
function describePlacement(placement: VpcPlacementStrategy) {
if (placement.subnetsToUse !== undefined) {
return `'${DEFAULT_SUBNET_NAME[placement.subnetsToUse]}' subnets`;
}
if (placement.subnetName !== undefined) {
return `subnets named '${placement.subnetName}'`;
}
return '???';
Copy link
Contributor

Choose a reason for hiding this comment

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

If you get there you've effectively reached the default behavior, I believe. Then you should probably return private subnets. If you are willing to assume that the defaults have always been reified before you get here, then why not throw instead of returning ????

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 was thinking to throw, but at the same time I didn't want to block a customer from using this construct if we ever forget a code path. Rather have ugly behavior but allow the user to continue (so that we can fix the issue when we have capacity) rather than block them completely by throwing an error and now we have to scramble to get a fix out.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right but then at least avoid the question marks... Maybe JSON.stringify what you've been given.

}
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,24 @@ export = {

test.done();
},

'selecting default subnets in a VPC with only public subnets'(test: Test) {
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
// GIVEN
const stack = new Stack();
const vpc = VpcNetwork.import(stack, 'VPC', {
vpcId: 'vpc-1234',
availabilityZones: ['dummy1a', 'dummy1b', 'dummy1c'],
publicSubnetIds: ['pub-1', 'pub-2', 'pub-3'],
});

test.throws(() => {
vpc.subnetIds();
}, /There are no 'Private' subnets in this VPC/);

test.done();
}
},

};

function getTestStack(): Stack {
Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/aws-ecs/lib/base/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,12 @@ export abstract class BaseService extends cdk.Construct
if (securityGroup === undefined) {
securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc });
}
const subnets = vpc.subnets(vpcPlacement);
this.connections.addSecurityGroup(securityGroup);

this.networkConfiguration = {
awsvpcConfiguration: {
assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED',
subnets: subnets.map(x => x.subnetId),
subnets: vpc.subnetIds(vpcPlacement),
securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]),
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class Cluster extends ClusterBase {

// Get subnetIds for all selected subnets
const placements = props.vpcPlacements || [{ subnetsToUse: ec2.SubnetType.Public }, { subnetsToUse: ec2.SubnetType.Private }];
const subnetIds = flatMap(placements, p => this.vpc.subnets(p)).map(s => s.subnetId);
const subnetIds = flatMap(placements, p => this.vpc.subnetIds(p));

const resource = new CfnCluster(this, 'Resource', {
name: props.clusterName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,23 @@ export abstract class BaseLoadBalancer extends cdk.Construct implements route53.

const internetFacing = ifUndefined(baseProps.internetFacing, false);

const subnets = baseProps.vpc.subnets(ifUndefined(baseProps.vpcPlacement,
{ subnetsToUse: internetFacing ? ec2.SubnetType.Public : ec2.SubnetType.Private }));
const vpcPlacement = ifUndefined(baseProps.vpcPlacement,
{ subnetsToUse: internetFacing ? ec2.SubnetType.Public : ec2.SubnetType.Private });

const subnets = baseProps.vpc.subnetIds(vpcPlacement);

this.vpc = baseProps.vpc;

const resource = new CfnLoadBalancer(this, 'Resource', {
name: baseProps.loadBalancerName,
subnets: subnets.map(s => s.subnetId),
subnets,
scheme: internetFacing ? 'internet-facing' : 'internal',
loadBalancerAttributes: new cdk.Token(() => renderAttributes(this.attributes)),
...additionalProps
});
if (internetFacing) {
resource.node.addDependency(...subnets.map(s => s.internetConnectivityEstablished));
const subnetObjs = baseProps.vpc.subnets(vpcPlacement);
resource.node.addDependency(...subnetObjs.map(s => s.internetConnectivityEstablished));
}

if (baseProps.deletionProtection) { this.setAttribute('deletion_protection.enabled', 'true'); }
Expand Down
8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,15 +504,21 @@ export class Function extends FunctionBase {

// Pick subnets, make sure they're not Public. Routing through an IGW
// won't work because the ENIs don't get a Public IP.
// Why are we not simply forcing vpcPlacement? Because you might still be choosing
// Isolated networks or selecting among 2 sets of Private subnets by name.
const subnets = props.vpc.subnets(props.vpcPlacement);
for (const subnet of subnets) {
if (props.vpc.isPublicSubnet(subnet)) {
throw new Error('Not possible to place Lambda Functions in a Public subnet');
}
}

// List can't be empty here, if we got this far you intended to put your Lambda
// in subnets. We're going to guarantee that we get the nice error message by
// making VpcNetwork do the selection again.

return {
subnetIds: subnets.map(s => s.subnetId),
subnetIds: props.vpc.subnetIds(props.vpcPlacement),
securityGroupIds: [securityGroup.securityGroupId]
};
}
Expand Down
10 changes: 6 additions & 4 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,16 @@ export class DatabaseCluster extends cdk.Construct implements IDatabaseCluster {
constructor(scope: cdk.Construct, id: string, props: DatabaseClusterProps) {
super(scope, id);

const subnets = props.instanceProps.vpc.subnets(props.instanceProps.vpcPlacement);
const subnetIds = props.instanceProps.vpc.subnetIds(props.instanceProps.vpcPlacement);

// Cannot test whether the subnets are in different AZs, but at least we can test the amount.
if (subnets.length < 2) {
throw new Error(`Cluster requires at least 2 subnets, got ${subnets.length}`);
if (subnetIds.length < 2) {
throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`);
}

const subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', {
dbSubnetGroupDescription: `Subnets for ${id} database`,
subnetIds: subnets.map(s => s.subnetId)
subnetIds,
});

const securityGroup = props.instanceProps.securityGroup !== undefined ?
Expand Down Expand Up @@ -187,6 +187,8 @@ export class DatabaseCluster extends cdk.Construct implements IDatabaseCluster {
throw new Error('At least one instance is required');
}

// Get the actual subnet objects so we can depend on internet connectivity.
const subnets = props.instanceProps.vpc.subnets(props.instanceProps.vpcPlacement);
for (let i = 0; i < instanceCount; i++) {
const instanceIndex = i + 1;

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/region-info/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/decdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.