Skip to content

Commit

Permalink
feat(ec2): user-defined subnet selectors (#10112)
Browse files Browse the repository at this point in the history
Continuation of #8526

This change adds a feature to SubnetSelection which lets the user provide objects that implements the new `ISubnetSelector` interface. These objects will be used by the VPC class to choose the subnets from the VPC.

This commit also provides an implementation of an ISubnetSelector that lets the user select subnets that contain IP addresses.

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
flemjame-at-amazon authored Sep 16, 2020
1 parent a726dad commit 491113d
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 45 deletions.
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Which subnets are selected is evaluated as follows:
in the given availability zones will be returned.
* `onePerAz`: per availability zone, a maximum of one subnet will be returned (Useful for resource
types that do not allow creating two ENIs in the same availability zone).
* `subnetFilters`: additional filtering on subnets using any number of user-provided filters which
extend the SubnetFilter class.

### Using NAT instances

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './network-acl';
export * from './network-acl-types';
export * from './port';
export * from './security-group';
export * from './subnet';
export * from './peer';
export * from './volume';
export * from './vpc';
Expand Down
115 changes: 115 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/subnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { CidrBlock, NetworkUtils } from './network-util';
import { ISubnet } from './vpc';

/**
* Contains logic which chooses a set of subnets from a larger list, in conjunction
* with SubnetSelection, to determine where to place AWS resources such as VPC
* endpoints, EC2 instances, etc.
*/
export abstract class SubnetFilter {

/**
* Chooses subnets which are in one of the given availability zones.
*/
public static availabilityZones(availabilityZones: string[]): SubnetFilter {
return new AvailabilityZoneSubnetFilter(availabilityZones);
}

/**
* Chooses subnets such that there is at most one per availability zone.
*/
public static onePerAz(): SubnetFilter {
return new OnePerAZSubnetFilter();
}

/**
* Chooses subnets which contain any of the specified IP addresses.
*/
public static containsIpAddresses(ipv4addrs: string[]): SubnetFilter {
return new ContainsIpAddressesSubnetFilter(ipv4addrs);
}

/**
* Executes the subnet filtering logic, returning a filtered set of subnets.
*/
public selectSubnets(_subnets: ISubnet[]): ISubnet[] {
throw new Error('Cannot select subnets with an abstract SubnetFilter. `selectSubnets` needs to be implmemented.');
}
}

/**
* Chooses subnets which are in one of the given availability zones.
*/
class AvailabilityZoneSubnetFilter extends SubnetFilter {

private readonly availabilityZones: string[];

constructor(availabilityZones: string[]) {
super();
this.availabilityZones = availabilityZones;
}

/**
* Executes the subnet filtering logic.
*/
public selectSubnets(subnets: ISubnet[]): ISubnet[] {
return subnets.filter(s => this.availabilityZones.includes(s.availabilityZone));
}
}

/**
* Chooses subnets such that there is at most one per availability zone.
*/
class OnePerAZSubnetFilter extends SubnetFilter {

constructor() {
super();
}

/**
* Executes the subnet filtering logic.
*/
public selectSubnets(subnets: ISubnet[]): ISubnet[] {
return this.retainOnePerAz(subnets);
}

private retainOnePerAz(subnets: ISubnet[]): ISubnet[] {
const azsSeen = new Set<string>();
return subnets.filter(subnet => {
if (azsSeen.has(subnet.availabilityZone)) { return false; }
azsSeen.add(subnet.availabilityZone);
return true;
});
}
}

/**
* Chooses subnets which contain any of the specified IP addresses.
*/
class ContainsIpAddressesSubnetFilter extends SubnetFilter {

private readonly ipAddresses: string[];

constructor(ipAddresses: string[]) {
super();
this.ipAddresses = ipAddresses;
}

/**
* Executes the subnet filtering logic.
*/
public selectSubnets(subnets: ISubnet[]): ISubnet[] {
return this.retainByIp(subnets, this.ipAddresses);
}

private retainByIp(subnets: ISubnet[], ips: string[]): ISubnet[] {
const cidrBlockObjs = ips.map(ip => {
const ipNum = NetworkUtils.ipToNum(ip);
return new CidrBlock(ipNum, 32);
});
return subnets.filter(s => {
const subnetCidrBlock = new CidrBlock(s.ipv4CidrBlock);
return cidrBlockObjs.some(cidr => subnetCidrBlock.containsCidr(cidr));
});
}
}
113 changes: 71 additions & 42 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { NatProvider } from './nat';
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
import { NetworkBuilder } from './network-util';
import { SubnetFilter } from './subnet';
import { allRouteTableIds, defaultSubnetName, flatten, ImportSubnetGroup, subnetGroupNameFromConstructId, subnetId } from './util';
import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions, InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint';
import { FlowLog, FlowLogOptions, FlowLogResourceType } from './vpc-flow-logs';
Expand All @@ -36,6 +37,11 @@ export interface ISubnet extends IResource {
*/
readonly internetConnectivityEstablished: IDependable;

/**
* The IPv4 CIDR block for this subnet
*/
readonly ipv4CidrBlock: string;

/**
* The route table for this subnet
*/
Expand Down Expand Up @@ -236,6 +242,13 @@ export interface SubnetSelection {
*/
readonly onePerAz?: boolean;

/**
* List of provided subnet filters.
*
* @default - none
*/
readonly subnetFilters?: SubnetFilter[];

/**
* Explicitly select individual subnets
*
Expand Down Expand Up @@ -460,17 +473,21 @@ abstract class VpcBase extends Resource implements IVpc {
subnets = this.selectSubnetObjectsByType(type);
}

if (selection.availabilityZones !== undefined) { // Filter by AZs, if specified
subnets = retainByAZ(subnets, selection.availabilityZones);
}

if (!!selection.onePerAz && subnets.length > 0) { // Ensure one per AZ if specified
subnets = retainOnePerAz(subnets);
}
// Apply all the filters
subnets = this.applySubnetFilters(subnets, selection.subnetFilters ?? []);

return subnets;
}

private applySubnetFilters(subnets: ISubnet[], filters: SubnetFilter[]): ISubnet[] {
let filtered = subnets;
// Apply each filter in sequence
for (const filter of filters) {
filtered = filter.selectSubnets(filtered);
}
return filtered;
}

private selectSubnetObjectsByName(groupName: string) {
const allSubnets = [...this.publicSubnets, ...this.privateSubnets, ...this.isolatedSubnets];
const subnets = allSubnets.filter(s => subnetGroupNameFromConstructId(s) === groupName);
Expand Down Expand Up @@ -510,9 +527,12 @@ abstract class VpcBase extends Resource implements IVpc {
* PUBLIC (in that order) that has any subnets.
*/
private reifySelectionDefaults(placement: SubnetSelection): SubnetSelection {

if (placement.subnetName !== undefined) {
if (placement.subnetGroupName !== undefined) {
throw new Error('Please use only \'subnetGroupName\' (\'subnetName\' is deprecated and has the same behavior)');
} else {
Annotations.of(this).addWarning('Usage of \'subnetName\' in SubnetSelection is deprecated, use \'subnetGroupName\' instead');
}
placement = { ...placement, subnetGroupName: placement.subnetName };
}
Expand All @@ -525,42 +545,27 @@ abstract class VpcBase extends Resource implements IVpc {

if (placement.subnetType === undefined && placement.subnetGroupName === undefined && placement.subnets === undefined) {
// Return default subnet type based on subnets that actually exist
if (this.privateSubnets.length > 0) {
return {
subnetType: SubnetType.PRIVATE,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones,
};
}
if (this.isolatedSubnets.length > 0) {
return {
subnetType: SubnetType.ISOLATED,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones,
};
}
return {
subnetType: SubnetType.PUBLIC,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones,
};
let subnetType = this.privateSubnets.length ? SubnetType.PRIVATE : this.isolatedSubnets.length ? SubnetType.ISOLATED : SubnetType.PUBLIC;
placement = { ...placement, subnetType: subnetType };
}

return placement;
}
}
// Establish which subnet filters are going to be used
let subnetFilters = placement.subnetFilters ?? [];

function retainByAZ(subnets: ISubnet[], azs: string[]): ISubnet[] {
return subnets.filter(s => azs.includes(s.availabilityZone));
}
// Backwards compatibility with existing `availabilityZones` and `onePerAz` functionality
if (placement.availabilityZones !== undefined) { // Filter by AZs, if specified
subnetFilters.push(SubnetFilter.availabilityZones(placement.availabilityZones));
}
if (!!placement.onePerAz) { // Ensure one per AZ if specified
subnetFilters.push(SubnetFilter.onePerAz());
}

// Overwrite the provided placement filters and remove the availabilityZones and onePerAz properties
placement = { ...placement, subnetFilters: subnetFilters, availabilityZones: undefined, onePerAz: undefined };
const { availabilityZones, onePerAz, ...rest } = placement;

function retainOnePerAz(subnets: ISubnet[]): ISubnet[] {
const azsSeen = new Set<string>();
return subnets.filter(subnet => {
if (azsSeen.has(subnet.availabilityZone)) { return false; }
azsSeen.add(subnet.availabilityZone);
return true;
});
return rest;
}
}

/**
Expand Down Expand Up @@ -654,6 +659,7 @@ export interface VpcAttributes {
}

export interface SubnetAttributes {

/**
* The Availability Zone the subnet is located in
*
Expand All @@ -662,16 +668,23 @@ export interface SubnetAttributes {
readonly availabilityZone?: string;

/**
* The subnetId for this particular subnet
* The IPv4 CIDR block associated with the subnet
*
* @default - No CIDR information, cannot use CIDR filter features
*/
readonly subnetId: string;
readonly ipv4CidrBlock?: string;

/**
* The ID of the route table for this particular subnet
*
* @default - No route table information, cannot create VPC endpoints
*/
readonly routeTableId?: string;

/**
* The subnetId for this particular subnet
*/
readonly subnetId: string;
}

/**
Expand Down Expand Up @@ -1442,6 +1455,11 @@ export class Subnet extends Resource implements ISubnet {
*/
public readonly availabilityZone: string;

/**
* @attribute
*/
public readonly ipv4CidrBlock: string;

/**
* The subnetId for this particular subnet
*/
Expand Down Expand Up @@ -1491,6 +1509,7 @@ export class Subnet extends Resource implements ISubnet {
Tags.of(this).add(NAME_TAG, this.node.path);

this.availabilityZone = props.availabilityZone;
this.ipv4CidrBlock = props.cidrBlock;
const subnet = new CfnSubnet(this, 'Subnet', {
vpcId: props.vpcId,
cidrBlock: props.cidrBlock,
Expand Down Expand Up @@ -1890,6 +1909,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
public readonly subnetId: string;
public readonly routeTable: IRouteTable;
private readonly _availabilityZone?: string;
private readonly _ipv4CidrBlock?: string;

constructor(scope: Construct, id: string, attrs: SubnetAttributes) {
super(scope, id);
Expand All @@ -1902,6 +1922,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
Annotations.of(this).addWarning(`No routeTableId was provided to the subnet ${ref}. Attempting to read its .routeTable.routeTableId will return null/undefined. (More info: https://github.com/aws/aws-cdk/pull/3171)`);
}

this._ipv4CidrBlock = attrs.ipv4CidrBlock;
this._availabilityZone = attrs.availabilityZone;
this.subnetId = attrs.subnetId;
this.routeTable = {
Expand All @@ -1913,11 +1934,19 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
public get availabilityZone(): string {
if (!this._availabilityZone) {
// eslint-disable-next-line max-len
throw new Error("You cannot reference a Subnet's availability zone if it was not supplied. Add the availabilityZone when importing using Subnet.fromSubnetAttributes()");
throw new Error('You cannot reference a Subnet\'s availability zone if it was not supplied. Add the availabilityZone when importing using Subnet.fromSubnetAttributes()');
}
return this._availabilityZone;
}

public get ipv4CidrBlock(): string {
if (!this._ipv4CidrBlock) {
// tslint:disable-next-line: max-line-length
throw new Error('You cannot reference an imported Subnet\'s IPv4 CIDR if it was not supplied. Add the ipv4CidrBlock when importing using Subnet.fromSubnetAttributes()');
}
return this._ipv4CidrBlock;
}

public associateNetworkAcl(id: string, networkAcl: INetworkAcl): void {
const scope = Construct.isConstruct(networkAcl) ? networkAcl : this;
const other = Construct.isConstruct(networkAcl) ? this : networkAcl;
Expand Down
Loading

0 comments on commit 491113d

Please sign in to comment.