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): user-defined subnet selectors #10112

Merged
merged 20 commits into from
Sep 16, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
446a54c
Merge pull request #21 from aws/master
flemjame-at-amazon Aug 31, 2020
61c6f96
Introduce subnet selector, didn't break any tests yet
flemjame-at-amazon Sep 1, 2020
638ec87
Move availability zone and onePerAz over to using the selector, no te…
flemjame-at-amazon Sep 1, 2020
c1cd7c0
Minor refactoring
flemjame-at-amazon Sep 1, 2020
8ba3b7b
Refactoring and rewording
flemjame-at-amazon Sep 2, 2020
48d6b56
re-order logic to be more like original - subnet name/type first, the…
flemjame-at-amazon Sep 2, 2020
9286b0e
Add subnet selection by IP
flemjame-at-amazon Sep 2, 2020
be6dabd
README update
flemjame-at-amazon Sep 2, 2020
d622b19
Merge branch 'master' into vpc-subnet-selection-protocol
flemjame-at-amazon Sep 2, 2020
0cc51f7
Remove double quotes being ignored by linter
flemjame-at-amazon Sep 8, 2020
32fdd47
Forgot to escape
flemjame-at-amazon Sep 8, 2020
5609f93
Change name to SubnetFilter
flemjame-at-amazon Sep 8, 2020
59af473
merge conflict
flemjame-at-amazon Sep 8, 2020
4fb96e2
Merge branch 'master' into vpc-subnet-selection-protocol
flemjame-at-amazon Sep 8, 2020
724b2a5
Change from interface to minimal abstract class
flemjame-at-amazon Sep 8, 2020
d0ca7c3
Merge branch 'master' into vpc-subnet-selection-protocol
flemjame-at-amazon Sep 8, 2020
d691e0c
Update README.md
flemjame-at-amazon Sep 9, 2020
84c6ae1
Remove azs and one per az from reifySelectionDefaults
flemjame-at-amazon Sep 11, 2020
cb5935b
Merge branch 'vpc-subnet-selection-protocol' of ssh://github.com/flem…
flemjame-at-amazon Sep 11, 2020
68a6b3b
Merge branch 'master' into vpc-subnet-selection-protocol
mergify[bot] Sep 16, 2020
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
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
implement the ISubnetSelector interface.

### 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
88 changes: 88 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/subnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 interface ISubnetSelector {

/**
* Executes the subnet filtering logic, returning a filtered set of subnets.
*/
selectSubnets(subnets: ISubnet[]): ISubnet[];
}

/**
* Chooses subnets which are in one of the given availability zones.
*/
export class AvailabilityZoneSubnetSelector implements ISubnetSelector {

private readonly availabilityZones: string[];

constructor(availabilityZones: string[]) {
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.
*/
export class OnePerAZSubnetSelector implements ISubnetSelector {

constructor() {}

/**
* 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.
*/
export class ContainsIpAddressesSubnetSelector implements ISubnetSelector {

private readonly ipAddresses: string[];

constructor(ipAddresses: string[]) {
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));
});
}
}
114 changes: 71 additions & 43 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import {
ConcreteDependable, Construct, ContextProvider, DependableTrait, IConstruct,
IDependable, IResource, Lazy, Resource, Stack, Token, Tags,
Annotations, ConcreteDependable, Construct, ContextProvider, DependableTrait,
IConstruct, IDependable, IResource, Lazy, Resource, Stack, Token, Tags,
} from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import {
Expand All @@ -11,6 +11,7 @@ import {
import { NatProvider } from './nat';
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
import { NetworkBuilder } from './network-util';
import { AvailabilityZoneSubnetSelector, OnePerAZSubnetSelector, ISubnetSelector } 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?: ISubnetSelector[];

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

Choose a reason for hiding this comment

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

Shouldn't this at least lead to the appropriate filter class being prepended to the list?

Strictly speaking this method is protected, therefore has a public contract to other people that could have inherited from this class and are relying on its current behavior.

Copy link
Contributor Author

@flemjame-at-amazon flemjame-at-amazon Sep 8, 2020

Choose a reason for hiding this comment

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

Yes you're correct. That functionality is now in reifySelectionDefaults. I did the same for the one-per-az selection option.

}

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: ISubnetSelector[]): 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');
Copy link
Contributor

Choose a reason for hiding this comment

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

This is API contract misuse and should definitely remain an error that aborts the program.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error still occurs if the user specifies both, like before. Now the user will get a friendly reminder that subnetName shouldn't be used, but still allows them to use it -- like before.

Should I change the behavior such that it throws an error?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see, I misread.

Well, hmm. I guess it's maybe a little overkill, but fine.

}
placement = { ...placement, subnetGroupName: placement.subnetName };
}
Expand All @@ -525,42 +545,26 @@ 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(new AvailabilityZoneSubnetSelector(placement.availabilityZones));
}
if (!!placement.onePerAz) { // Ensure one per AZ if specified
subnetFilters.push(new OnePerAZSubnetSelector());
}

// Overwrite the provided placement filters
placement = { ...placement, subnetFilters: subnetFilters };
Copy link
Contributor

Choose a reason for hiding this comment

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

This leaves the existing .availabilityZones and .onePerAz booleans in place.

Are you sure they're not going to be interpreted again?

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 did remove their interpretation everywhere else but here, but I suppose there's no harm in removing them completely.


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 placement;
}
}

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

export interface SubnetAttributes {

/**
* The Availability Zone the subnet is located in
*
Expand All @@ -662,16 +667,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 +1454,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 +1508,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 +1908,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 +1921,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
scope.node.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 @@ -1918,6 +1938,14 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
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