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

feat(ec2): allow filtering out exhausted subnets during subnet selection #8311

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
46 changes: 43 additions & 3 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export interface ISubnet extends IResource {
*/
readonly routeTable: IRouteTable;

/**
* Count of remaining IP addresses in the subnet
*/
readonly availableIpAddressCount?: number;

/**
* Associate a Network ACL with this subnet
*
Expand Down Expand Up @@ -244,6 +249,16 @@ export interface SubnetSelection {
* @default - Use all subnets in a selected group (all private subnets by default)
*/
readonly subnets?: ISubnet[]

/**
* Filter out Subnets with 0 available IP addresses
*
* Usful to avoid deployments to IP exhausted subnets that are a part of the selected
* subnet group.
*
* @default false
*/
readonly filterOutIpExhausted?: boolean;
}

/**
Expand Down Expand Up @@ -464,6 +479,10 @@ abstract class VpcBase extends Resource implements IVpc {
subnets = retainOnePerAz(subnets);
}

if (selection.filterOutIpExhausted) { // Filter out IP exhausted
subnets = retainOnlyIpAvailable(subnets);
}

return subnets;
}

Expand Down Expand Up @@ -525,18 +544,21 @@ abstract class VpcBase extends Resource implements IVpc {
return {
subnetType: SubnetType.PRIVATE,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones};
availabilityZones: placement.availabilityZones,
filterOutIpExhausted: placement.filterOutIpExhausted };
}
if (this.isolatedSubnets.length > 0) {
return {
subnetType: SubnetType.ISOLATED,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones };
availabilityZones: placement.availabilityZones,
filterOutIpExhausted: placement.filterOutIpExhausted };
}
return {
subnetType: SubnetType.PUBLIC,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones };
availabilityZones: placement.availabilityZones,
filterOutIpExhausted: placement.filterOutIpExhausted };
}

return placement;
Expand All @@ -556,6 +578,14 @@ function retainOnePerAz(subnets: ISubnet[]): ISubnet[] {
});
}

function retainOnlyIpAvailable(subnets: ISubnet[]): ISubnet[] {
return subnets.filter(subnet => {
if (subnet.availableIpAddressCount! === 0) { return false; } // Be optimistic if no count defined for backwards compat.

return true;
});
}

/**
* Properties that reference an external Vpc
*/
Expand Down Expand Up @@ -665,6 +695,13 @@ export interface SubnetAttributes {
* @default - No route table information, cannot create VPC endpoints
*/
readonly routeTableId?: string;

/**
* Count of remaining available IP addresses in the subnet
*
* @default - No available ip address count available
*/
readonly availableIpAddressCount?: number;
}

/**
Expand Down Expand Up @@ -1800,6 +1837,7 @@ class LookedUpVpc extends VpcBase {
availabilityZone: vpcSubnet.availabilityZone,
subnetId: vpcSubnet.subnetId,
routeTableId: vpcSubnet.routeTableId,
availableIpAddressCount: vpcSubnet.availableIpAddressCount!,
}));
}
return ret;
Expand Down Expand Up @@ -1850,6 +1888,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
public readonly internetConnectivityEstablished: IDependable = new ConcreteDependable();
public readonly subnetId: string;
public readonly routeTable: IRouteTable;
public readonly availableIpAddressCount?: number;
private readonly _availabilityZone?: string;

constructor(scope: Construct, id: string, attrs: SubnetAttributes) {
Expand All @@ -1869,6 +1908,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
// Forcing routeTableId to pretend non-null to maintain backwards-compatibility. See https://github.com/aws/aws-cdk/pull/3171
routeTableId: attrs.routeTableId!,
};
this.availableIpAddressCount = attrs.availableIpAddressCount!;
}

public get availabilityZone(): string {
Expand Down
145 changes: 144 additions & 1 deletion packages/@aws-cdk/aws-ec2/test/test.vpc.from-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { Construct, ContextProvider, GetContextValueOptions, GetContextValueResult, Lazy, Stack } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { Test } from 'nodeunit';
import { GenericLinuxImage, Instance, InstanceType, SubnetType, Vpc } from '../lib';
import { GenericLinuxImage, Instance, InstanceType, ISubnet, SubnetType, Vpc } from '../lib';

export = {
'Vpc.fromLookup()': {
Expand Down Expand Up @@ -104,6 +104,149 @@ export = {
test.done();
},

'populates subnet available ip count info'(test: Test) {
const previous = mockVpcContextProviderWith(test, {
vpcId: 'vpc-1234',
subnetGroups: [
{
name: 'Public',
type: cxapi.VpcSubnetGroupType.PUBLIC,
subnets: [
{
subnetId: 'pub-exh-sub-in-us-east-1a',
availabilityZone: 'us-east-1a',
routeTableId: 'rt-123',
availableIpAddressCount: 0,
},
{
subnetId: 'pub-sub-in-us-east-1b',
availabilityZone: 'us-east-1b',
routeTableId: 'rt-123',
},
],
},
{
name: 'Private',
type: cxapi.VpcSubnetGroupType.PRIVATE,
subnets: [
{
subnetId: 'pri-sub-1-in-us-east-1c',
availabilityZone: 'us-east-1c',
routeTableId: 'rt-123',
},
{
subnetId: 'pri-exh-sub-2-in-us-east-1c',
availabilityZone: 'us-east-1c',
routeTableId: 'rt-123',
availableIpAddressCount: 0,
},
{
subnetId: 'pri-ip-avail-sub-1-in-us-east-1d',
availabilityZone: 'us-east-1d',
routeTableId: 'rt-123',
availableIpAddressCount: 10,
},
{
subnetId: 'pri-sub-2-in-us-east-1d',
availabilityZone: 'us-east-1d',
routeTableId: 'rt-123',
},
],
},
],
}, options => {
test.deepEqual(options.filter, {
isDefault: 'true',
});

test.equal(options.subnetGroupNameTag, undefined);
});

const stack = new Stack();
const vpc = Vpc.fromLookup(stack, 'Vpc', {
isDefault: true,
});

test.deepEqual(vpc.availabilityZones, ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d']);
test.equal(vpc.publicSubnets.length, 2);
test.equal(vpc.privateSubnets.length, 4);
test.equal(vpc.isolatedSubnets.length, 0);

const filterSubnets = (subnets: ISubnet[], filterExpression: ((x: ISubnet) => boolean)) => subnets.filter(subnet => filterExpression(subnet));
const ipExhaustedSubnet = (subnet: ISubnet) => subnet.availableIpAddressCount! === 0;
const explicitlyIpAvailableSubnet = (subnet: ISubnet) => subnet.availableIpAddressCount! > 0;
const implicitlyIpAvailableSubnet = (subnet: ISubnet) => !ipExhaustedSubnet(subnet) && !explicitlyIpAvailableSubnet(subnet);

test.strictEqual(filterSubnets(vpc.publicSubnets, ipExhaustedSubnet).length, 1);
test.strictEqual(filterSubnets(vpc.publicSubnets, explicitlyIpAvailableSubnet).length, 0);
test.strictEqual(filterSubnets(vpc.publicSubnets, implicitlyIpAvailableSubnet).length, 1);
test.strictEqual(filterSubnets(vpc.privateSubnets, ipExhaustedSubnet).length, 1);
test.strictEqual(filterSubnets(vpc.privateSubnets, explicitlyIpAvailableSubnet).length, 1);
test.strictEqual(filterSubnets(vpc.privateSubnets, implicitlyIpAvailableSubnet).length, 2);

restoreContextProvider(previous);
test.done();
},

'filters out IP exhausted subnets'(test: Test) {
const exhaustedSubnetId: string = 'pri-exh-sub-2-in-us-east-1c';

const previous = mockVpcContextProviderWith(test, {
vpcId: 'vpc-1234',
subnetGroups: [
{
name: 'Private',
type: cxapi.VpcSubnetGroupType.PRIVATE,
subnets: [
{
subnetId: 'pri-sub-1-in-us-east-1c',
availabilityZone: 'us-east-1c',
routeTableId: 'rt-123',
},
{
subnetId: exhaustedSubnetId,
availabilityZone: 'us-east-1c',
routeTableId: 'rt-123',
availableIpAddressCount: 0,
},
{
subnetId: 'pri-ip-avail-sub-1-in-us-east-1d',
availabilityZone: 'us-east-1d',
routeTableId: 'rt-123',
availableIpAddressCount: 10,
},
{
subnetId: 'pri-sub-2-in-us-east-1d',
availabilityZone: 'us-east-1d',
routeTableId: 'rt-123',
},
],
},
],
}, options => {
test.deepEqual(options.filter, {
isDefault: 'true',
});

test.equal(options.subnetGroupNameTag, undefined);
});

const stack = new Stack();
const vpc = Vpc.fromLookup(stack, 'Vpc', {
isDefault: true,
});

const { subnetIds } = vpc.selectSubnets({ filterOutIpExhausted: true });

test.equal(vpc.privateSubnets.length, 4);

test.equal(subnetIds.length, 3);
test.ok(!subnetIds.includes(exhaustedSubnetId));

restoreContextProvider(previous);
test.done();
},

'selectSubnets onePerAz works on imported VPC'(test: Test) {
const previous = mockVpcContextProviderWith(test, {
vpcId: 'vpc-1234',
Expand Down
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,30 @@ export = {
test.done();
},

'select non-lookup subnets with ip exhaustion filter'(test: Test) {
const isExpectedSubnet = (expectedSubnetIds: string[]) => (subnetId: string) => expectedSubnetIds.includes(subnetId);

// GIVEN
const stack = getTestStack();
const vpc = new Vpc(stack, 'VpcNetwork', {
maxAzs: 1,
subnetConfiguration: [
{name: 'lb', subnetType: SubnetType.PUBLIC },
{name: 'app', subnetType: SubnetType.PRIVATE },
{name: 'db', subnetType: SubnetType.PRIVATE },
],
});
const privateSubnets = vpc.privateSubnets.map(subnet => subnet.subnetId);

// WHEN
const { subnetIds } = vpc.selectSubnets({ filterOutIpExhausted: true });

// THEN
test.deepEqual(subnetIds.length, 2);
test.ok(subnetIds.every(isExpectedSubnet(privateSubnets)));
test.done();
},

'select explicitly defined subnets'(test: Test) {
// GIVEN
const stack = getTestStack();
Expand Down
7 changes: 7 additions & 0 deletions packages/@aws-cdk/cx-api/lib/context/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export interface VpcSubnet {
* @default - CIDR information not available
*/
readonly cidr?: string;

/**
* Count of IP addresses still available in the subnet.
*
* @default - Available IP address count not available
*/
readonly availableIpAddressCount?: number;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/aws-cdk/lib/context-providers/vpcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin {
name,
subnetId: subnet.SubnetId!,
routeTableId,
availableIpAddressCount: subnet.AvailableIpAddressCount!,
};
});

Expand Down Expand Up @@ -232,6 +233,7 @@ function groupAsymmetricSubnets(subnets: Subnet[]): cxapi.VpcSubnetGroup[] {
cidr: subnet.cidr,
availabilityZone: subnet.az,
routeTableId: subnet.routeTableId,
availableIpAddressCount: subnet.availableIpAddressCount!,
})),
};
});
Expand Down Expand Up @@ -264,6 +266,7 @@ interface Subnet {
name: string;
routeTableId: string;
subnetId: string;
availableIpAddressCount: number;
}

interface SubnetGroup {
Expand Down