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): select subnets that contain IP address(es) #8526

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
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 @@ -142,6 +142,8 @@ Which subnets are selected is evaluated as follows:
availability zones have been looked up).
* `availabilityZones`: only the specific subnets from the selected subnet groups that are
in the given availability zones will be returned.
* `containsIPv4Addr`: only the specific subnets from the selected subnet groups that contain
any of the provided IPv4 addresses 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).

Expand Down
64 changes: 58 additions & 6 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation } from './ec2.generated';
import { NatProvider } from './nat';
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
import { NetworkBuilder } from './network-util';
import { CidrBlock, NetworkBuilder, NetworkUtils } from './network-util';
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 @@ -33,6 +33,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 @@ -196,6 +201,13 @@ export interface SubnetSelection {
*/
readonly availabilityZones?: string[];

/**
* Select subnets whose CIDR range contains any of the provided IPv4 addresses
*
* @default no filtering on IP is done
*/
readonly containsIPv4Addr?: string[];

/**
* Select the subnet group with the given name
*
Expand Down Expand Up @@ -460,6 +472,10 @@ abstract class VpcBase extends Resource implements IVpc {
subnets = retainByAZ(subnets, selection.availabilityZones);
}

if (selection.containsIPv4Addr !== undefined) { // Filter by IP blocks, inclusive
subnets = retainByIp(subnets, selection.containsIPv4Addr);
}

if (!!selection.onePerAz && subnets.length > 0) { // Ensure one per AZ if specified
subnets = retainOnePerAz(subnets);
}
Expand Down Expand Up @@ -525,24 +541,38 @@ abstract class VpcBase extends Resource implements IVpc {
return {
subnetType: SubnetType.PRIVATE,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones};
availabilityZones: placement.availabilityZones,
containsIPv4Addr: placement.containsIPv4Addr };
}
if (this.isolatedSubnets.length > 0) {
return {
subnetType: SubnetType.ISOLATED,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones };
availabilityZones: placement.availabilityZones,
containsIPv4Addr: placement.containsIPv4Addr };
}
return {
subnetType: SubnetType.PUBLIC,
onePerAz: placement.onePerAz,
availabilityZones: placement.availabilityZones };
availabilityZones: placement.availabilityZones,
containsIPv4Addr: placement.containsIPv4Addr };
}

return placement;
}
}

function 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));
});
}

function retainByAZ(subnets: ISubnet[], azs: string[]): ISubnet[] {
return subnets.filter(s => azs.includes(s.availabilityZone));
}
Expand Down Expand Up @@ -654,6 +684,13 @@ export interface SubnetAttributes {
*/
readonly availabilityZone?: string;

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

/**
* The subnetId for this particular subnet
*/
Expand Down Expand Up @@ -1415,6 +1452,11 @@ export class Subnet extends Resource implements ISubnet {
*/
public readonly subnetAvailabilityZone: string;

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

/**
* @attribute
*/
Expand Down Expand Up @@ -1449,6 +1491,7 @@ export class Subnet extends Resource implements ISubnet {
this.node.applyAspect(new Tag(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 @@ -1851,6 +1894,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 @@ -1862,7 +1906,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
// tslint:disable-next-line: max-line-length
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 @@ -1871,10 +1915,18 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
};
}

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 get availabilityZone(): string {
if (!this._availabilityZone) {
// tslint:disable-next-line: max-line-length
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 an imported Subnet's availability zone if it was not supplied. Add the availabilityZone when importing using Subnet.fromSubnetAttributes()");
}
return this._availabilityZone;
}
Expand Down
70 changes: 69 additions & 1 deletion packages/@aws-cdk/aws-ec2/test/test.vpc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { countResources, expect, haveResource, haveResourceLike, isSuperObject, MatchStyle } from '@aws-cdk/assert';
import { CfnOutput, Lazy, Stack, Tag } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { AclCidr, AclTraffic, CfnSubnet, CfnVPC, DefaultInstanceTenancy, GenericLinuxImage, InstanceType, InterfaceVpcEndpoint,
import { AclCidr, AclTraffic, BastionHostLinux, CfnSubnet, CfnVPC, DefaultInstanceTenancy, GenericLinuxImage, InstanceType, InterfaceVpcEndpoint,
InterfaceVpcEndpointService, NatProvider, NetworkAcl, NetworkAclEntry, Peer, Port, PrivateSubnet, PublicSubnet,
RouterType, Subnet, SubnetType, TrafficDirection, Vpc } from '../lib';

Expand Down Expand Up @@ -1219,6 +1219,74 @@ export = {
test.done();
},

'can filter by single IP address'(test: Test) {
// GIVEN
const stack = getTestStack();

// IP space is split into 6 pieces, one public/one private per AZ
const vpc = new Vpc(stack, 'VPC', {
cidr: '10.0.0.0/16',
maxAzs: 3,
});

// WHEN
// We want to place this bastion host in the same subnet as this IPv4
// address.
new BastionHostLinux(stack, 'Bastion', {
vpc,
subnetSelection: {
containsIPv4Addr: ['10.0.160.0'],
},
});

// THEN
// 10.0.160.0/19 is the third subnet, sequentially, if you split
// 10.0.0.0/16 into 6 pieces
expect(stack).to(haveResource('AWS::EC2::Instance', {
SubnetId: {
Ref: 'VPCPrivateSubnet3Subnet3EDCD457',
},
}));
test.done();
},

'can filter by multiple IP addresses'(test: Test) {
// GIVEN
const stack = getTestStack();

// IP space is split into 6 pieces, one public/one private per AZ
const vpc = new Vpc(stack, 'VPC', {
cidr: '10.0.0.0/16',
maxAzs: 3,
});

// WHEN
// We want to place this endpoint in the same subnets as these IPv4
// address.
// WHEN
new InterfaceVpcEndpoint(stack, 'VPC Endpoint', {
vpc,
service: new InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443),
subnets: {
containsIPv4Addr: ['10.0.96.0', '10.0.160.0'],
},
});

// THEN
expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', {
ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc',
SubnetIds: [
{
Ref: 'VPCPrivateSubnet1Subnet8BCA10E0',
},
{
Ref: 'VPCPrivateSubnet3Subnet3EDCD457',
},
],
}));
test.done();
},

},
};

Expand Down