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