packages/@aws-cdk/aws-ec2-alpha/lib/subnet-v2.ts (256 lines of code) (raw):
import { Resource, Names, Lazy, Tags, Token, ValidationError, UnscopedValidationError } from 'aws-cdk-lib';
import { CfnSubnet, CfnSubnetRouteTableAssociation, INetworkAcl, IRouteTable, ISubnet, NetworkAcl, SubnetNetworkAclAssociation, SubnetType } from 'aws-cdk-lib/aws-ec2';
import { Construct, DependencyGroup, IDependable } from 'constructs';
import { IVpcV2 } from './vpc-v2-base';
import { CidrBlock, CidrBlockIpv6 } from './util';
import { RouteTable } from './route';
import { addConstructMetadata, MethodMetadata } from 'aws-cdk-lib/core/lib/metadata-resource';
/**
* Interface to define subnet CIDR
*/
interface ICidr {
readonly cidr: string;
}
/**
* IPv4 or IPv6 CIDR range for the subnet
*/
export class IpCidr implements ICidr {
/**
* IPv6 CIDR range for the subnet
* Allowed only if IPv6 is enabled on VPc
*/
public readonly cidr: string;
constructor(props: string ) {
this.cidr = props;
}
}
/**
* Name tag constant
*/
const NAME_TAG: string = 'Name';
/**
* VPC Name tag constant
*/
const VPCNAME_TAG: string = 'VpcName';
/**
* Properties to define subnet for VPC.
*/
export interface SubnetV2Props {
/**
* VPC Prop
*/
readonly vpc: IVpcV2;
/**
* ipv4 cidr to assign to this subnet.
* See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#cfn-ec2-subnet-cidrblock
*/
readonly ipv4CidrBlock: IpCidr;
/**
* Ipv6 CIDR Range for subnet
*
* @default - No Ipv6 address
*/
readonly ipv6CidrBlock?: IpCidr;
/**
* Custom AZ for the subnet
*/
readonly availabilityZone: string;
/**
* Custom Route for subnet
*
* @default - a default route table created
*/
readonly routeTable?: IRouteTable;
/**
* The type of Subnet to configure.
*
* The Subnet type will control the ability to route and connect to the
* Internet.
*
* TODO: Add validation check `subnetType` when adding resources (e.g. cannot add NatGateway to private)
*/
readonly subnetType: SubnetType;
/**
* Subnet name
*
* @default - provisioned with an autogenerated name by CDK
*/
readonly subnetName?: string;
/**
* Indicates whether a network interface created in this subnet receives an IPv6 address.
* If you specify AssignIpv6AddressOnCreation, you must also specify Ipv6CidrBlock.
*
* @default - undefined in case not provided as an input
*/
readonly assignIpv6AddressOnCreation?: boolean;
/**
* Controls if instances launched into the subnet should be assigned a public IP address.
* This property can only be set for public subnets.
*
* @default - undefined in case not provided as an input
*/
readonly mapPublicIpOnLaunch?: boolean;
}
/**
* Interface with additional properties for SubnetV2
*/
export interface ISubnetV2 extends ISubnet {
/**
* The IPv6 CIDR block for this subnet
*/
readonly ipv6CidrBlock?: string;
/**
* The type of subnet (public or private) that this subnet represents.
*
* @attribute SubnetType
*/
readonly subnetType?: SubnetType;
}
/**
* The SubnetV2 class represents a subnet within a VPC (Virtual Private Cloud) in AWS.
* It extends the Resource class and implements the ISubnet interface.
*
* Instances of this class can be used to create and manage subnets within a VpcV2 instance.
* Subnets can be configured with specific IP address ranges (IPv4 and IPv6), availability zones,
* and subnet types (e.g., public, private, isolated).
*
* @resource AWS::EC2::Subnet
*
*/
export class SubnetV2 extends Resource implements ISubnetV2 {
/**
* Import an existing subnet to the VPC
*/
public static fromSubnetV2Attributes(scope: Construct, id: string, attrs: SubnetV2Attributes) : ISubnetV2 {
/**
* Class to define an import for an existing subnet
* @resource AWS::EC2::Subnet
*/
class ImportedSubnetV2 extends Resource implements ISubnetV2 {
/**
* The IPv6 CIDR Block assigned to this subnet
*/
public readonly ipv6CidrBlock?: string = attrs.ipv6CidrBlock;
/**
* The type of subnet (eg. public or private) that this subnet represents.
*/
public readonly subnetType?: SubnetType = attrs.subnetType;
/**
* The Availability Zone in which subnet is located
*/
public readonly availabilityZone: string = attrs.availabilityZone;
/**
* The subnetId for this particular subnet
* Refers to the physical ID created
*/
public readonly subnetId: string = attrs.subnetId;
/**
* Dependable that can be depended upon to force internet connectivity established on the VPC
*/
public readonly internetConnectivityEstablished: IDependable = new DependencyGroup();
/**
* The IPv4 CIDR block assigned to this subnet
*/
public readonly ipv4CidrBlock: string = attrs.ipv4CidrBlock;
/**
* Current route table associated with this subnet
*/
public readonly routeTable: IRouteTable = { routeTableId: attrs.routeTableId! };
/**
* Associate a Network ACL with this subnet
* Required here since it is implemented in the ISubnetV2
*/
public associateNetworkAcl(aclId: string, networkAcl: INetworkAcl) {
const aclScope = networkAcl instanceof Construct ? networkAcl : this;
const other = networkAcl instanceof Construct ? this : networkAcl;
new SubnetNetworkAclAssociation(aclScope, aclId + Names.nodeUniqueId(other.node), {
networkAcl,
subnet: this,
});
}
}
return new ImportedSubnetV2(scope, id);
}
/**
* The Availability Zone the subnet is located in
*/
public readonly availabilityZone: string;
/**
* The subnetId for this particular subnet
* @attribute
*/
public readonly subnetId: string;
/**
* Dependencies for internet connectivity
* This Property exposes the RouteTable-Subnet association so that other resources can depend on it.
*/
public readonly internetConnectivityEstablished: IDependable;
/**
* The variable name `internetConnectivityEstablished` does not reflect what it actually is.
* The naming is enforced by ISubnet. We need to keep it to maintain compatibility.
* It exposes the RouteTable-Subnet association so that other resources can depend on it.
* E.g. Resources in a subnet, when being deleted, may need the RouteTable to exist in order to delete properly
*/
private readonly _internetConnectivityEstablished = new DependencyGroup();
/**
* The IPv4 CIDR block for this subnet
*/
public readonly ipv4CidrBlock: string;
/**
* The IPv6 CIDR Block for this subnet
*/
public readonly ipv6CidrBlock?: string;
/**
* The type of subnet (public or private) that this subnet represents.
* @attribute SubnetType
*/
public readonly subnetType?: SubnetType;
private _networkAcl: INetworkAcl;
private _routeTable: IRouteTable;
/**
* Constructs a new SubnetV2 instance.
* @param scope The parent Construct that this resource will be part of.
* @param id The unique identifier for this resource.
* @param props The configuration properties for the subnet.
*/
constructor(scope: Construct, id: string, props: SubnetV2Props) {
super(scope, id, {
physicalName: props.subnetName ?? Lazy.string({
produce: () => Names.uniqueResourceName(this, { maxLength: 128, allowedSpecialCharacters: '_' }),
}),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
const ipv4CidrBlock = props.ipv4CidrBlock.cidr;
const ipv6CidrBlock = props.ipv6CidrBlock?.cidr;
if (!checkCidrRanges(props.vpc, props.ipv4CidrBlock.cidr)) {
throw new ValidationError('CIDR block should be within the range of VPC', this);
}
let overlap: boolean = false;
let overlapIpv6: boolean = false;
if (!Token.isUnresolved(props.ipv4CidrBlock)) {
overlap = validateOverlappingCidrRanges(props.vpc, props.ipv4CidrBlock.cidr);
}
// check whether VPC supports ipv6
if (props.ipv6CidrBlock?.cidr && !Token.isUnresolved(props.ipv6CidrBlock?.cidr)) {
validateSupportIpv6(props.vpc);
overlapIpv6 = validateOverlappingCidrRangesipv6(props.vpc, props.ipv6CidrBlock?.cidr);
}
if (overlap || overlapIpv6) {
throw new ValidationError('CIDR block should not overlap with existing subnet blocks', this);
}
if (props.assignIpv6AddressOnCreation && !props.ipv6CidrBlock) {
throw new ValidationError('IPv6 CIDR block is required when assigning IPv6 address on creation', this);
}
if (props.mapPublicIpOnLaunch === true && props.subnetType !== SubnetType.PUBLIC) {
throw new ValidationError('mapPublicIpOnLaunch can only be set to true for public subnets', this);
}
const subnet = new CfnSubnet(this, 'Subnet', {
vpcId: props.vpc.vpcId,
cidrBlock: ipv4CidrBlock,
ipv6CidrBlock: ipv6CidrBlock,
availabilityZone: props.availabilityZone,
assignIpv6AddressOnCreation: props.assignIpv6AddressOnCreation,
mapPublicIpOnLaunch: props.mapPublicIpOnLaunch,
});
this.node.defaultChild = subnet;
this.ipv4CidrBlock = props.ipv4CidrBlock.cidr;
this.ipv6CidrBlock = props.ipv6CidrBlock?.cidr;
this.subnetId = subnet.ref;
this.availabilityZone = props.availabilityZone;
this._networkAcl = NetworkAcl.fromNetworkAclId(this, 'Acl', subnet.attrNetworkAclAssociationId);
if (props.subnetName) {
Tags.of(this).add(NAME_TAG, props.subnetName);
}
if (props.vpc.vpcName) {
Tags.of(this).add(VPCNAME_TAG, props.vpc.vpcName);
}
if (props.routeTable) {
this._routeTable = props.routeTable;
} else {
// Assigning a default route table
this._routeTable = new RouteTable(this, 'RouteTable', {
vpc: props.vpc,
routeTableName: 'DefaultCDKRouteTable',
});
}
const routeAssoc = new CfnSubnetRouteTableAssociation(this, 'RouteTableAssociation', {
subnetId: this.subnetId,
routeTableId: this.routeTable.routeTableId,
});
this._internetConnectivityEstablished.add(routeAssoc);
this.internetConnectivityEstablished = this._internetConnectivityEstablished;
this.subnetType = props.subnetType;
storeSubnetToVpcByType(props.vpc, this, props.subnetType);
}
/**
* Associate a Network ACL with this subnet
*
* @param id The unique identifier for this association.
* @param networkAcl The Network ACL to associate with this subnet.
* This allows controlling inbound and outbound traffic for instances in this subnet.
*/
@MethodMetadata()
public associateNetworkAcl(id: string, networkAcl: INetworkAcl) {
this._networkAcl = networkAcl;
const scope = networkAcl instanceof Construct ? networkAcl : this;
const other = networkAcl instanceof Construct ? this : networkAcl;
new SubnetNetworkAclAssociation(scope, id + Names.nodeUniqueId(other.node), {
networkAcl,
subnet: this,
});
}
/**
* Return the Route Table associated with this subnet
*/
public get routeTable(): IRouteTable {
return this._routeTable;
}
/**
* Returns the Network ACL associated with this subnet.
*/
public get networkAcl(): INetworkAcl {
return this._networkAcl;
}
}
/**
* Properties required to import a subnet
*/
export interface SubnetV2Attributes {
/**
* The Availability Zone this subnet is located in
*
* @default - No AZ information, cannot use AZ selection features
*/
readonly availabilityZone: string;
/**
* The IPv4 CIDR block associated with the subnet
*
* @default - No CIDR information, cannot use CIDR filter features
*/
readonly ipv4CidrBlock: string;
/**
* The IPv4 CIDR block associated with the subnet
*
* @default - No CIDR information, cannot use CIDR filter features
*/
readonly ipv6CidrBlock?: 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;
/**
* The type of subnet (public or private) that this subnet represents.
*/
readonly subnetType: SubnetType;
/**
* Name of the given subnet
*
* @default - no subnet name
*/
readonly subnetName?: string;
}
type DeprecatedSubnetType = 'Deprecated_Isolated' | 'Deprecated_Private';
const subnetTypeMap: { [key in SubnetType | DeprecatedSubnetType]: (vpc: IVpcV2, subnet: SubnetV2) => void } = {
[SubnetType.PRIVATE_ISOLATED]: (vpc: IVpcV2, subnet: SubnetV2) => vpc.isolatedSubnets.push(subnet),
[SubnetType.PUBLIC]: (vpc: IVpcV2, subnet: SubnetV2) => vpc.publicSubnets.push(subnet),
[SubnetType.PRIVATE_WITH_EGRESS]: (vpc: IVpcV2, subnet: SubnetV2) => vpc.privateSubnets.push(subnet),
['Deprecated_Isolated']: (vpc: IVpcV2, subnet: SubnetV2) => vpc.isolatedSubnets.push(subnet),
['Deprecated_Private']: (vpc: IVpcV2, subnet: SubnetV2) => vpc.privateSubnets.push(subnet),
[SubnetType.PRIVATE_WITH_NAT]: (vpc: IVpcV2, subnet: SubnetV2) => vpc.privateSubnets.push(subnet),
};
/**
* Stores the provided subnet in the VPC's collection of subnets based on the specified subnet type.
*
* @param vpc The VPC instance to which the subnet belongs.
* @param subnet The subnet instance to be stored.
* @param type The type of the subnet (e.g., public, private, isolated).
* @internal
*/
function storeSubnetToVpcByType(vpc: IVpcV2, subnet: SubnetV2, type: SubnetType) {
const findFunctionType = subnetTypeMap[type];
if (findFunctionType) {
findFunctionType(vpc, subnet);
} else {
throw new UnscopedValidationError(`Unsupported subnet type: ${type}`);
}
/**
* Need to set explicit dependency as during stack deletion,
* the cidr blocks may get deleted first and will fail as the subnets are still using the cidr blocks
*/
if (vpc.secondaryCidrBlock) {
for (const cidr of vpc.secondaryCidrBlock) {
subnet.node.addDependency(cidr);
}
}
}
/**
* Validates whether the provided VPC supports IPv6 addresses.
*
* @param vpc The VPC instance to be validated.
* @throws ValidationError if the VPC does not support IPv6 addresses.
* @returns True if the VPC supports IPv6 addresses, false otherwise.
* @internal
*/
function validateSupportIpv6(vpc: IVpcV2) {
if (vpc.secondaryCidrBlock) {
if (vpc.secondaryCidrBlock.some((secondaryAddress) => secondaryAddress.amazonProvidedIpv6CidrBlock === true ||
secondaryAddress.ipv6IpamPoolId !== undefined || secondaryAddress.ipv6Pool !== undefined)) {
return true;
} else {
throw new UnscopedValidationError('To use IPv6, the VPC must enable IPv6 support.');
}
} else {return false;}
}
/**
* Checks if the provided CIDR range falls within the IP address ranges of the given VPC.
*
* @param vpc The VPC instance to check against.
* @param cidrRange The CIDR range to be checked.
* @returns True if the CIDR range falls within the VPC's IP address ranges, false otherwise.
* @internal
*/
function checkCidrRanges(vpc: IVpcV2, cidrRange: string) {
const vpcCidrBlock = [vpc.ipv4CidrBlock];
const subnetCidrBlock = new CidrBlock(cidrRange);
const allCidrs: CidrBlock[] = [];
// Secondary IP addresses assoicated using user defined IPv4 range
if (vpc.secondaryCidrBlock) {
for (const ipAddress of vpc.secondaryCidrBlock) {
if (ipAddress.cidrBlock) {
vpcCidrBlock.push(ipAddress.cidrBlock);
}
}
const cidrs = vpcCidrBlock.map(cidr => new CidrBlock(cidr));
allCidrs.push(...cidrs);
}
// Secondary IP addresses assoicated using IPAM IPv4 range
if (vpc.ipv4IpamProvisionedCidrs) {
const cidrs = vpc.ipv4IpamProvisionedCidrs.map(cidr => new CidrBlock(cidr));
allCidrs.push(...cidrs);
}
// If no IPv4 is assigned as secondary address
if (allCidrs.length === 0) {
throw new UnscopedValidationError('No secondary IP address attached to VPC');
}
return allCidrs.some(c => c.containsCidr(subnetCidrBlock));
}
/**
* Validates if the provided IPv4 CIDR block overlaps with existing subnet CIDR blocks within the given VPC.
*
* @param vpc The VPC instance to check against.
* @param ipv4CidrBlock The IPv4 CIDR block to be validated.
* @returns True if the IPv4 CIDR block overlaps with existing subnet CIDR blocks, false otherwise.
* @internal
*/
function validateOverlappingCidrRanges(vpc: IVpcV2, ipv4CidrBlock: string): boolean {
let allSubnets: ISubnetV2[];
try {
allSubnets = vpc.selectSubnets().subnets;
} catch (e) {
'No subnets in VPC';
return false;
}
const ipMap: [string, string][] = new Array();
const inputRange = new CidrBlock(ipv4CidrBlock);
const inputIpMap: [string, string] = [inputRange.minIp(), inputRange.maxIp()];
for (const subnet of allSubnets) {
const cidrBlock = new CidrBlock(subnet.ipv4CidrBlock);
ipMap.push([cidrBlock.minIp(), cidrBlock.maxIp()]);
}
for (const range of ipMap) {
if (inputRange.rangesOverlap(range, inputIpMap)) {
return true;
}
}
return false;
}
/**
* Validates if the provided IPv6 CIDR block overlaps with existing subnet CIDR blocks within the given VPC.
*
* @param vpc The VPC instance to check against.
* @param ipv6CidrBlock The IPv6 CIDR block to be validated.
* @returns True if the IPv6 CIDR block overlaps with existing subnet CIDR blocks, false otherwise.
* @throws ValidationError if no subnets are found in the VPC.
* @internal
*/
function validateOverlappingCidrRangesipv6(vpc: IVpcV2, ipv6CidrBlock: string): boolean {
let allSubnets: ISubnetV2[];
try {
allSubnets = vpc.selectSubnets().subnets;
} catch (e) {
'No subnets in VPC';
return false;
}
const ipv6Map: string[]= [];
const inputRange = new CidrBlockIpv6(ipv6CidrBlock);
let result : boolean = false;
for (const subnet of allSubnets) {
if (subnet.ipv6CidrBlock) {
const cidrBlock = new CidrBlockIpv6(subnet.ipv6CidrBlock);
ipv6Map.push(cidrBlock.cidr);
}
}
for (const range of ipv6Map) {
if (inputRange.rangesOverlap(range, inputRange.cidr)) {
result = true;
}
}
return result;
}