packages/aws-cdk-lib/aws-ec2/lib/vpc.ts (1,214 lines of code) (raw):

import { Construct, Dependable, DependencyGroup, IConstruct, IDependable, Node } from 'constructs'; import { ClientVpnEndpoint, ClientVpnEndpointOptions } from './client-vpn-endpoint'; import { CfnEIP, CfnEgressOnlyInternetGateway, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCCidrBlock, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation, } from './ec2.generated'; import { AllocatedSubnet, IIpAddresses, RequestedSubnet, IpAddresses, IIpv6Addresses, Ipv6Addresses } from './ip-addresses'; import { NatProvider } from './nat'; import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl'; 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'; import { VpcLookupOptions } from './vpc-lookup'; import { EnableVpnGatewayOptions, VpnConnection, VpnConnectionOptions, VpnConnectionType, VpnGateway } from './vpn'; import * as cxschema from '../../cloud-assembly-schema'; import { Arn, Annotations, ContextProvider, IResource, Fn, Lazy, Resource, Stack, Token, Tags, Names, CustomResource, FeatureFlags, ValidationError, UnscopedValidationError, } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import { RestrictDefaultSgProvider } from '../../custom-resource-handlers/dist/aws-ec2/restrict-default-sg-provider.generated'; import * as cxapi from '../../cx-api'; import { EC2_RESTRICT_DEFAULT_SECURITY_GROUP } from '../../cx-api'; const VPC_SUBNET_SYMBOL = Symbol.for('@aws-cdk/aws-ec2.VpcSubnet'); const FAKE_AZ_NAME = 'fake-az'; export interface ISubnet extends IResource { /** * The Availability Zone the subnet is located in */ readonly availabilityZone: string; /** * The subnetId for this particular subnet * @attribute */ readonly subnetId: string; /** * Dependable that can be depended upon to force internet connectivity established on the VPC */ readonly internetConnectivityEstablished: IDependable; /** * The IPv4 CIDR block for this subnet */ readonly ipv4CidrBlock: string; /** * The route table for this subnet */ readonly routeTable: IRouteTable; /** * Associate a Network ACL with this subnet * * @param acl The Network ACL to associate */ associateNetworkAcl(id: string, acl: INetworkAcl): void; } /** * An abstract route table */ export interface IRouteTable { /** * Route table ID */ readonly routeTableId: string; } export interface IVpc extends IResource { /** * Identifier for this VPC * @attribute */ readonly vpcId: string; /** * ARN for this VPC * @attribute */ readonly vpcArn: string; /** * CIDR range for this VPC * * @attribute */ readonly vpcCidrBlock: string; /** * List of public subnets in this VPC */ readonly publicSubnets: ISubnet[]; /** * List of private subnets in this VPC */ readonly privateSubnets: ISubnet[]; /** * List of isolated subnets in this VPC */ readonly isolatedSubnets: ISubnet[]; /** * AZs for this VPC */ readonly availabilityZones: string[]; /** * Identifier for the VPN gateway */ readonly vpnGatewayId?: string; /** * Dependable that can be depended upon to force internet connectivity established on the VPC */ readonly internetConnectivityEstablished: IDependable; /** * Return information on the subnets appropriate for the given selection strategy * * Requires that at least one subnet is matched, throws a descriptive * error message otherwise. */ selectSubnets(selection?: SubnetSelection): SelectedSubnets; /** * Adds a VPN Gateway to this VPC */ enableVpnGateway(options: EnableVpnGatewayOptions): void; /** * Adds a new VPN connection to this VPC */ addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection; /** * Adds a new client VPN endpoint to this VPC */ addClientVpnEndpoint(id: string, options: ClientVpnEndpointOptions): ClientVpnEndpoint; /** * Adds a new gateway endpoint to this VPC */ addGatewayEndpoint(id: string, options: GatewayVpcEndpointOptions): GatewayVpcEndpoint; /** * Adds a new interface endpoint to this VPC */ addInterfaceEndpoint(id: string, options: InterfaceVpcEndpointOptions): InterfaceVpcEndpoint; /** * Adds a new Flow Log to this VPC */ addFlowLog(id: string, options?: FlowLogOptions): FlowLog; } /** * The types of IP addresses provisioned in the VPC. */ export enum IpProtocol { /** * The vpc will be configured with only IPv4 addresses. * * This is the default protocol if unset. */ IPV4_ONLY = 'Ipv4_Only', /** * The vpc will have both IPv4 and IPv6 addresses. * * Unless specified, public IPv4 addresses will not be auto assigned, * an egress only internet gateway (EIGW) will be created and configured, * and NATs and internet gateways (IGW) will be configured with IPv6 addresses. */ DUAL_STACK = 'Dual_Stack', } /** * The type of Subnet */ export enum SubnetType { /** * Isolated Subnets do not route traffic to the Internet (in this VPC), * and as such, do not require NAT gateways. * * Isolated subnets can only connect to or be connected to from other * instances in the same VPC. A default VPC configuration will not include * isolated subnets. * * This can be good for subnets with RDS or Elasticache instances, * or which route Internet traffic through a peer VPC. */ PRIVATE_ISOLATED = 'Isolated', /** * Isolated Subnets do not route traffic to the Internet (in this VPC), * and as such, do not require NAT gateways. * * Isolated subnets can only connect to or be connected to from other * instances in the same VPC. A default VPC configuration will not include * isolated subnets. * * This can be good for subnets with RDS or Elasticache instances, * or which route Internet traffic through a peer VPC. * * @deprecated use `SubnetType.PRIVATE_ISOLATED` */ ISOLATED = 'Deprecated_Isolated', /** * Subnet that routes to the internet, but not vice versa. * * Instances in a private subnet can connect to the Internet, but will not * allow connections to be initiated from the Internet. Egress to the internet will * need to be provided. * NAT Gateway(s) are the default solution to providing this subnet type the ability to route Internet traffic. * If a NAT Gateway is not required or desired, set `natGateways:0` or use * `SubnetType.PRIVATE_ISOLATED` instead. * * By default, a NAT gateway is created in every public subnet for maximum availability. * Be aware that you will be charged for NAT gateways. * * Normally a Private subnet will use a NAT gateway in the same AZ, but * if `natGateways` is used to reduce the number of NAT gateways, a NAT * gateway from another AZ will be used instead. */ PRIVATE_WITH_EGRESS = 'Private', /** * Subnet that routes to the internet (via a NAT gateway), but not vice versa. * * Instances in a private subnet can connect to the Internet, but will not * allow connections to be initiated from the Internet. NAT Gateway(s) are * required with this subnet type to route the Internet traffic through. * If a NAT Gateway is not required or desired, use `SubnetType.PRIVATE_ISOLATED` instead. * * By default, a NAT gateway is created in every public subnet for maximum availability. * Be aware that you will be charged for NAT gateways. * * Normally a Private subnet will use a NAT gateway in the same AZ, but * if `natGateways` is used to reduce the number of NAT gateways, a NAT * gateway from another AZ will be used instead. * @deprecated use `PRIVATE_WITH_EGRESS` */ PRIVATE_WITH_NAT = 'Deprecated_Private_NAT', /** * Subnet that routes to the internet, but not vice versa. * * Instances in a private subnet can connect to the Internet, but will not * allow connections to be initiated from the Internet. NAT Gateway(s) are * required with this subnet type to route the Internet traffic through. * If a NAT Gateway is not required or desired, use `SubnetType.PRIVATE_ISOLATED` instead. * * By default, a NAT gateway is created in every public subnet for maximum availability. * Be aware that you will be charged for NAT gateways. * * Normally a Private subnet will use a NAT gateway in the same AZ, but * if `natGateways` is used to reduce the number of NAT gateways, a NAT * gateway from another AZ will be used instead. * * @deprecated use `PRIVATE_WITH_EGRESS` */ PRIVATE = 'Deprecated_Private', /** * Subnet connected to the Internet * * Instances in a Public subnet can connect to the Internet and can be * connected to from the Internet as long as they are launched with public * IPs (controlled on the AutoScalingGroup or other constructs that launch * instances). * * Public subnets route outbound traffic via an Internet Gateway. */ PUBLIC = 'Public', } /** * Customize subnets that are selected for placement of ENIs * * Constructs that allow customization of VPC placement use parameters of this * type to provide placement settings. * * By default, the instances are placed in the private subnets. */ export interface SubnetSelection { /** * Select all subnets of the given type * * At most one of `subnetType` and `subnetGroupName` can be supplied. * * @default SubnetType.PRIVATE_WITH_EGRESS (or ISOLATED or PUBLIC if there are no PRIVATE_WITH_EGRESS subnets) */ readonly subnetType?: SubnetType; /** * Select subnets only in the given AZs. * * @default no filtering on AZs is done */ readonly availabilityZones?: string[]; /** * Select the subnet group with the given name * * Select the subnet group with the given name. This only needs * to be used if you have multiple subnet groups of the same type * and you need to distinguish between them. Otherwise, prefer * `subnetType`. * * This field does not select individual subnets, it selects all subnets that * share the given subnet group name. This is the name supplied in * `subnetConfiguration`. * * At most one of `subnetType` and `subnetGroupName` can be supplied. * * @default - Selection by type instead of by name */ readonly subnetGroupName?: string; /** * Alias for `subnetGroupName` * * Select the subnet group with the given name. This only needs * to be used if you have multiple subnet groups of the same type * and you need to distinguish between them. * * @deprecated Use `subnetGroupName` instead */ readonly subnetName?: string; /** * If true, return at most one subnet per AZ * * @default false */ readonly onePerAz?: boolean; /** * List of provided subnet filters. * * @default - none */ readonly subnetFilters?: SubnetFilter[]; /** * Explicitly select individual subnets * * Use this if you don't want to automatically use all subnets in * a group, but have a need to control selection down to * individual subnets. * * Cannot be specified together with `subnetType` or `subnetGroupName`. * * @default - Use all subnets in a selected group (all private subnets by default) */ readonly subnets?: ISubnet[]; } /** * Result of selecting a subset of subnets from a VPC */ export interface SelectedSubnets { /** * The subnet IDs */ readonly subnetIds: string[]; /** * The respective AZs of each subnet */ readonly availabilityZones: string[]; /** * Dependency representing internet connectivity for these subnets */ readonly internetConnectivityEstablished: IDependable; /** * Selected subnet objects */ readonly subnets: ISubnet[]; /** * Whether any of the given subnets are from the VPC's public subnets. */ readonly hasPublic: boolean; /** * The subnet selection is not actually real yet * * If this value is true, don't validate anything about the subnets. The count * or identities are not known yet, and the validation will most likely fail * which will prevent a successful lookup. * * @default false */ readonly isPendingLookup?: boolean; } /** * A new or imported VPC */ abstract class VpcBase extends Resource implements IVpc { /** * Identifier for this VPC */ public abstract readonly vpcId: string; /** * Arn of this VPC */ public abstract readonly vpcArn: string; /** * CIDR range for this VPC */ public abstract readonly vpcCidrBlock: string; /** * List of public subnets in this VPC */ public abstract readonly publicSubnets: ISubnet[]; /** * List of private subnets in this VPC */ public abstract readonly privateSubnets: ISubnet[]; /** * List of isolated subnets in this VPC */ public abstract readonly isolatedSubnets: ISubnet[]; /** * AZs for this VPC */ public abstract readonly availabilityZones: string[]; /** * Dependencies for internet connectivity */ public abstract readonly internetConnectivityEstablished: IDependable; /** * Dependencies for NAT connectivity * * @deprecated - This value is no longer used. */ protected readonly natDependencies = new Array<IConstruct>(); /** * If this is set to true, don't error out on trying to select subnets */ protected incompleteSubnetDefinition: boolean = false; /** * Mutable private field for the vpnGatewayId * * @internal */ protected _vpnGatewayId?: string; /** * Returns IDs of selected subnets */ public selectSubnets(selection: SubnetSelection = {}): SelectedSubnets { const subnets = this.selectSubnetObjects(selection); const pubs = new Set(this.publicSubnets); return { subnetIds: subnets.map(s => s.subnetId), get availabilityZones(): string[] { return subnets.map(s => s.availabilityZone); }, internetConnectivityEstablished: tap(new CompositeDependable(), d => subnets.forEach(s => d.add(s.internetConnectivityEstablished))), subnets, hasPublic: subnets.some(s => pubs.has(s)), isPendingLookup: this.incompleteSubnetDefinition, }; } /** * Adds a VPN Gateway to this VPC */ public enableVpnGateway(options: EnableVpnGatewayOptions): void { if (this.vpnGatewayId) { throw new ValidationError('The VPN Gateway has already been enabled.', this); } const vpnGateway = new VpnGateway(this, 'VpnGateway', { amazonSideAsn: options.amazonSideAsn, type: VpnConnectionType.IPSEC_1, }); this._vpnGatewayId = vpnGateway.gatewayId; const attachment = new CfnVPCGatewayAttachment(this, 'VPCVPNGW', { vpcId: this.vpcId, vpnGatewayId: this._vpnGatewayId, }); // Propagate routes on route tables associated with the right subnets const vpnRoutePropagation = options.vpnRoutePropagation ?? [{}]; const routeTableIds = allRouteTableIds(flatten(vpnRoutePropagation.map(s => this.selectSubnets(s).subnets))); if (routeTableIds.length === 0) { Annotations.of(this).addError(`enableVpnGateway: no subnets matching selection: '${JSON.stringify(vpnRoutePropagation)}'. Select other subnets to add routes to.`); } const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { routeTableIds, vpnGatewayId: this._vpnGatewayId, }); // The AWS::EC2::VPNGatewayRoutePropagation resource cannot use the VPN gateway // until it has successfully attached to the VPC. // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html routePropagation.node.addDependency(attachment); } /** * Adds a new VPN connection to this VPC */ public addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection { return new VpnConnection(this, id, { vpc: this, ...options, }); } /** * Adds a new client VPN endpoint to this VPC */ public addClientVpnEndpoint(id: string, options: ClientVpnEndpointOptions): ClientVpnEndpoint { return new ClientVpnEndpoint(this, id, { ...options, vpc: this, }); } /** * Adds a new interface endpoint to this VPC */ public addInterfaceEndpoint(id: string, options: InterfaceVpcEndpointOptions): InterfaceVpcEndpoint { return new InterfaceVpcEndpoint(this, id, { vpc: this, ...options, }); } /** * Adds a new gateway endpoint to this VPC */ public addGatewayEndpoint(id: string, options: GatewayVpcEndpointOptions): GatewayVpcEndpoint { return new GatewayVpcEndpoint(this, id, { vpc: this, ...options, }); } /** * Adds a new flow log to this VPC */ public addFlowLog(id: string, options?: FlowLogOptions): FlowLog { return new FlowLog(this, id, { resourceType: FlowLogResourceType.fromVpc(this), ...options, }); } /** * Returns the id of the VPN Gateway (if enabled) */ public get vpnGatewayId(): string | undefined { return this._vpnGatewayId; } /** * Return the subnets appropriate for the placement strategy */ protected selectSubnetObjects(selection: SubnetSelection = {}): ISubnet[] { selection = this.reifySelectionDefaults(selection); if (selection.subnets !== undefined) { return selection.subnets; } let subnets; if (selection.subnetGroupName !== undefined) { // Select by name subnets = this.selectSubnetObjectsByName(selection.subnetGroupName); } else { // Or specify by type const type = selection.subnetType || SubnetType.PRIVATE_WITH_EGRESS; subnets = this.selectSubnetObjectsByType(type); } // 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); if (subnets.length === 0 && !this.incompleteSubnetDefinition) { const names = Array.from(new Set(allSubnets.map(subnetGroupNameFromConstructId))); throw new ValidationError(`There are no subnet groups with name '${groupName}' in this VPC. Available names: ${names}`, this); } return subnets; } private selectSubnetObjectsByType(subnetType: SubnetType) { const allSubnets = { [SubnetType.PRIVATE_ISOLATED]: this.isolatedSubnets, [SubnetType.ISOLATED]: this.isolatedSubnets, [SubnetType.PRIVATE_WITH_NAT]: this.privateSubnets, [SubnetType.PRIVATE_WITH_EGRESS]: this.privateSubnets, [SubnetType.PRIVATE]: this.privateSubnets, [SubnetType.PUBLIC]: this.publicSubnets, }; const subnets = allSubnets[subnetType]; // Force merge conflict here with https://github.com/aws/aws-cdk/pull/4089 // see ImportedVpc if (subnets.length === 0 && !this.incompleteSubnetDefinition) { const availableTypes = Object.entries(allSubnets).filter(([_, subs]) => subs.length > 0).map(([typeName, _]) => typeName); throw new ValidationError(`There are no '${subnetType}' subnet groups in this VPC. Available types: ${availableTypes}`, this); } return subnets; } /** * Validate the fields in a SubnetSelection object, and reify defaults if necessary * * In case of default selection, select the first type of PRIVATE, ISOLATED, * PUBLIC (in that order) that has any subnets. */ private reifySelectionDefaults(placement: SubnetSelection): SubnetSelection { if (placement.subnetName !== undefined) { if (placement.subnetGroupName !== undefined) { throw new ValidationError('Please use only \'subnetGroupName\' (\'subnetName\' is deprecated and has the same behavior)', this); } else { Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:subnetNameDeprecated', 'Usage of \'subnetName\' in SubnetSelection is deprecated, use \'subnetGroupName\' instead'); } placement = { ...placement, subnetGroupName: placement.subnetName }; } const exclusiveSelections: Array<keyof SubnetSelection> = ['subnets', 'subnetType', 'subnetGroupName']; const providedSelections = exclusiveSelections.filter(key => placement[key] !== undefined); if (providedSelections.length > 1) { throw new ValidationError(`Only one of '${providedSelections}' can be supplied to subnet selection.`, this); } if (placement.subnetType === undefined && placement.subnetGroupName === undefined && placement.subnets === undefined) { // Return default subnet type based on subnets that actually exist let subnetType = this.privateSubnets.length ? SubnetType.PRIVATE_WITH_EGRESS : this.isolatedSubnets.length ? SubnetType.PRIVATE_ISOLATED : SubnetType.PUBLIC; placement = { ...placement, subnetType: subnetType }; } // Establish which subnet filters are going to be used let subnetFilters = placement.subnetFilters ?? []; // 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; return rest; } } /** * Properties that reference an external Vpc */ export interface VpcAttributes { /** * VPC's identifier */ readonly vpcId: string; /** * VPC's CIDR range * * @default - Retrieving the CIDR from the VPC will fail */ readonly vpcCidrBlock?: string; /** * List of availability zones for the subnets in this VPC. */ readonly availabilityZones: string[]; /** * List of public subnet IDs * * Must be undefined or match the availability zones in length and order. * * @default - The VPC does not have any public subnets */ readonly publicSubnetIds?: string[]; /** * List of names for the public subnets * * Must be undefined or have a name for every public subnet group. * * @default - All public subnets will have the name `Public` */ readonly publicSubnetNames?: string[]; /** * List of IDs of route tables for the public subnets. * * Must be undefined or have a name for every public subnet group. * * @default - Retrieving the route table ID of any public subnet will fail */ readonly publicSubnetRouteTableIds?: string[]; /** * List of IPv4 CIDR blocks for the public subnets. * * Must be undefined or have an entry for every public subnet group. * * @default - Retrieving the IPv4 CIDR block of any public subnet will fail */ readonly publicSubnetIpv4CidrBlocks?: string[]; /** * List of private subnet IDs * * Must be undefined or match the availability zones in length and order. * * @default - The VPC does not have any private subnets */ readonly privateSubnetIds?: string[]; /** * List of names for the private subnets * * Must be undefined or have a name for every private subnet group. * * @default - All private subnets will have the name `Private` */ readonly privateSubnetNames?: string[]; /** * List of IDs of route tables for the private subnets. * * Must be undefined or have a name for every private subnet group. * * @default - Retrieving the route table ID of any private subnet will fail */ readonly privateSubnetRouteTableIds?: string[]; /** * List of IPv4 CIDR blocks for the private subnets. * * Must be undefined or have an entry for every private subnet group. * * @default - Retrieving the IPv4 CIDR block of any private subnet will fail */ readonly privateSubnetIpv4CidrBlocks?: string[]; /** * List of isolated subnet IDs * * Must be undefined or match the availability zones in length and order. * * @default - The VPC does not have any isolated subnets */ readonly isolatedSubnetIds?: string[]; /** * List of names for the isolated subnets * * Must be undefined or have a name for every isolated subnet group. * * @default - All isolated subnets will have the name `Isolated` */ readonly isolatedSubnetNames?: string[]; /** * List of IDs of route tables for the isolated subnets. * * Must be undefined or have a name for every isolated subnet group. * * @default - Retrieving the route table ID of any isolated subnet will fail */ readonly isolatedSubnetRouteTableIds?: string[]; /** * List of IPv4 CIDR blocks for the isolated subnets. * * Must be undefined or have an entry for every isolated subnet group. * * @default - Retrieving the IPv4 CIDR block of any isolated subnet will fail */ readonly isolatedSubnetIpv4CidrBlocks?: string[]; /** * VPN gateway's identifier */ readonly vpnGatewayId?: string; /** * The region the VPC is in * * @default - The region of the stack where the VPC belongs to */ readonly region?: string; } export interface SubnetAttributes { /** * The Availability Zone the 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 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; } /** * Name tag constant */ const NAME_TAG: string = 'Name'; /** * Configuration for Vpc */ export interface VpcProps { /** * The protocol of the vpc. * * Options are IPv4 only or dual stack. * * @default IpProtocol.IPV4_ONLY */ readonly ipProtocol?: IpProtocol; /** * The Provider to use to allocate IPv4 Space to your VPC. * * Options include static allocation or from a pool. * * Note this is specific to IPv4 addresses. * * @default ec2.IpAddresses.cidr */ readonly ipAddresses?: IIpAddresses; /** * The CIDR range to use for the VPC, e.g. '10.0.0.0/16'. * * Should be a minimum of /28 and maximum size of /16. The range will be * split across all subnets per Availability Zone. * * @default Vpc.DEFAULT_CIDR_RANGE * * @deprecated Use ipAddresses instead */ readonly cidr?: string; /** * Indicates whether the instances launched in the VPC get public DNS hostnames. * * If this attribute is true, instances in the VPC get public DNS hostnames, * but only if the enableDnsSupport attribute is also set to true. * * @default true */ readonly enableDnsHostnames?: boolean; /** * Indicates whether the DNS resolution is supported for the VPC. * * If this attribute is false, the Amazon-provided DNS server in the VPC that * resolves public DNS hostnames to IP addresses is not enabled. If this * attribute is true, queries to the Amazon provided DNS server at the * 169.254.169.253 IP address, or the reserved IP address at the base of the * VPC IPv4 network range plus two will succeed. * * @default true */ readonly enableDnsSupport?: boolean; /** * The default tenancy of instances launched into the VPC. * * By setting this to dedicated tenancy, instances will be launched on * hardware dedicated to a single AWS customer, unless specifically specified * at instance launch time. Please note, not all instance types are usable * with Dedicated tenancy. * * @default DefaultInstanceTenancy.Default (shared) tenancy */ readonly defaultInstanceTenancy?: DefaultInstanceTenancy; /** * Define the maximum number of AZs to use in this region * * If the region has more AZs than you want to use (for example, because of * EIP limits), pick a lower number here. The AZs will be sorted and picked * from the start of the list. * * If you pick a higher number than the number of AZs in the region, all AZs * in the region will be selected. To use "all AZs" available to your * account, use a high number (such as 99). * * Be aware that environment-agnostic stacks will be created with access to * only 2 AZs, so to use more than 2 AZs, be sure to specify the account and * region on your stack. * * Specify this option only if you do not specify `availabilityZones`. * * @default 3 */ readonly maxAzs?: number; /** * Define the number of AZs to reserve. * * When specified, the IP space is reserved for the azs but no actual * resources are provisioned. * * @default 0 */ readonly reservedAzs?: number; /** * Availability zones this VPC spans. * * Specify this option only if you do not specify `maxAzs`. * * @default - a subset of AZs of the stack */ readonly availabilityZones?: string[]; /** * The number of NAT Gateways/Instances to create. * * The type of NAT gateway or instance will be determined by the * `natGatewayProvider` parameter. * * You can set this number lower than the number of Availability Zones in your * VPC in order to save on NAT cost. Be aware you may be charged for * cross-AZ data traffic instead. * * @default - One NAT gateway/instance per Availability Zone */ readonly natGateways?: number; /** * Configures the subnets which will have NAT Gateways/Instances * * You can pick a specific group of subnets by specifying the group name; * the picked subnets must be public subnets. * * Only necessary if you have more than one public subnet group. * * @default - All public subnets. */ readonly natGatewaySubnets?: SubnetSelection; /** * What type of NAT provider to use * * Select between NAT gateways or NAT instances. NAT gateways * may not be available in all AWS regions. * * @default NatProvider.gateway() * */ readonly natGatewayProvider?: NatProvider; /** * Configure the subnets to build for each AZ * * Each entry in this list configures a Subnet Group; each group will contain a * subnet for each Availability Zone. * * For example, if you want 1 public subnet, 1 private subnet, and 1 isolated * subnet in each AZ provide the following: * * ```ts * new ec2.Vpc(this, 'VPC', { * subnetConfiguration: [ * { * cidrMask: 24, * name: 'ingress', * subnetType: ec2.SubnetType.PUBLIC, * }, * { * cidrMask: 24, * name: 'application', * subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, * }, * { * cidrMask: 28, * name: 'rds', * subnetType: ec2.SubnetType.PRIVATE_ISOLATED, * } * ] * }); * ``` * * @default - The VPC CIDR will be evenly divided between 1 public and 1 * private subnet per AZ. */ readonly subnetConfiguration?: SubnetConfiguration[]; /** * Indicates whether a VPN gateway should be created and attached to this VPC. * * @default - true when vpnGatewayAsn or vpnConnections is specified */ readonly vpnGateway?: boolean; /** * The private Autonomous System Number (ASN) for the VPN gateway. * * @default - Amazon default ASN. */ readonly vpnGatewayAsn?: number; /** * VPN connections to this VPC. * * @default - No connections. */ readonly vpnConnections?: { [id: string]: VpnConnectionOptions }; /** * Where to propagate VPN routes. * * @default - On the route tables associated with private subnets. If no * private subnets exists, isolated subnets are used. If no isolated subnets * exists, public subnets are used. */ readonly vpnRoutePropagation?: SubnetSelection[]; /** * Gateway endpoints to add to this VPC. * * @default - None. */ readonly gatewayEndpoints?: { [id: string]: GatewayVpcEndpointOptions }; /** * Flow logs to add to this VPC. * * @default - No flow logs. */ readonly flowLogs?: { [id: string]: FlowLogOptions }; /** * The VPC name. * * Since the VPC resource doesn't support providing a physical name, the value provided here will be recorded in the `Name` tag * * @default this.node.path */ readonly vpcName?: string; /** * If set to true then the default inbound & outbound rules will be removed * from the default security group * * @default true if '@aws-cdk/aws-ec2:restrictDefaultSecurityGroup' is enabled, false otherwise */ readonly restrictDefaultSecurityGroup?: boolean; /** * If set to false then disable the creation of the default internet gateway * * @default true */ readonly createInternetGateway?: boolean; /** * The Provider to use to allocate IPv6 Space to your VPC. * * Options include amazon provided CIDR block. * * Note this is specific to IPv6 addresses. * * @default Ipv6Addresses.amazonProvided */ readonly ipv6Addresses?: IIpv6Addresses; } /** * The default tenancy of instances launched into the VPC. */ export enum DefaultInstanceTenancy { /** * Instances can be launched with any tenancy. */ DEFAULT = 'default', /** * Any instance launched into the VPC automatically has dedicated tenancy, unless you launch it with the default tenancy. */ DEDICATED = 'dedicated', /** * Instances must be launched on dedicated hosts. This provides additional visibility * and control over instance placement at the physical host level. */ HOST = 'host', } /** * Specify configuration parameters for a single subnet group in a VPC. */ export interface SubnetConfiguration { /** * The number of leading 1 bits in the routing mask. * * The number of available IP addresses in each subnet of this group * will be equal to `2^(32 - cidrMask) - 2`. * * Valid values are `16--28`. * * Note this is specific to IPv4 addresses. * * @default - Available IP space is evenly divided across subnets. */ readonly cidrMask?: number; /** * The type of Subnet to configure. * * The Subnet type will control the ability to route and connect to the * Internet. */ readonly subnetType: SubnetType; /** * Logical name for the subnet group. * * This name can be used when selecting VPC subnets to distinguish * between different subnet groups of the same type. */ readonly name: string; /** * Controls if subnet IP space needs to be reserved. * * When true, the IP space for the subnet is reserved but no actual * resources are provisioned. This space is only dependent on the * number of availability zones and on `cidrMask` - all other subnet * properties are ignored. * * @default false */ readonly reserved?: boolean; /** * Controls if a public IPv4 address is associated to an instance at launch * * Note this is specific to IPv4 addresses. * * @default true in Subnet.Public of IPV4_ONLY VPCs, false otherwise */ readonly mapPublicIpOnLaunch?: boolean; /** * This property is specific to dual stack VPCs. * * If set to false, then an IPv6 address will not be automatically assigned. * * Note this is specific to IPv6 addresses. * * @default true */ readonly ipv6AssignAddressOnCreation?: boolean; } /** * Define an AWS Virtual Private Cloud * * See the package-level documentation of this package for an overview * of the various dimensions in which you can configure your VPC. * * For example: * * ```ts * const vpc = new ec2.Vpc(this, 'TheVPC', { * ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), * }) * * // Iterate the private subnets * const selection = vpc.selectSubnets({ * subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS * }); * * for (const subnet of selection.subnets) { * // ... * } * ``` * * @resource AWS::EC2::VPC */ export class Vpc extends VpcBase { /** * The default CIDR range used when creating VPCs. * This can be overridden using VpcProps when creating a VPCNetwork resource. * e.g. new VpcResource(this, { cidr: '192.168.0.0./16' }) * * Note this is specific to the IPv4 CIDR. */ public static readonly DEFAULT_CIDR_RANGE: string = '10.0.0.0/16'; /** * The default subnet configuration * * 1 Public and 1 Private subnet per AZ evenly split */ public static readonly DEFAULT_SUBNETS: SubnetConfiguration[] = [ { subnetType: SubnetType.PUBLIC, name: defaultSubnetName(SubnetType.PUBLIC), }, { subnetType: SubnetType.PRIVATE_WITH_EGRESS, name: defaultSubnetName(SubnetType.PRIVATE_WITH_EGRESS), }, ]; /** * The default subnet configuration if natGateways specified to be 0 * * 1 Public and 1 Isolated Subnet per AZ evenly split */ public static readonly DEFAULT_SUBNETS_NO_NAT: SubnetConfiguration[] = [ { subnetType: SubnetType.PUBLIC, name: defaultSubnetName(SubnetType.PUBLIC), }, { subnetType: SubnetType.PRIVATE_ISOLATED, name: defaultSubnetName(SubnetType.PRIVATE_ISOLATED), }, ]; /** * Import a VPC by supplying all attributes directly * * NOTE: using `fromVpcAttributes()` with deploy-time parameters (like a `Fn.importValue()` or * `CfnParameter` to represent a list of subnet IDs) sometimes accidentally works. It happens * to work for constructs that need a list of subnets (like `AutoScalingGroup` and `eks.Cluster`) * but it does not work for constructs that need individual subnets (like * `Instance`). See https://github.com/aws/aws-cdk/issues/4118 for more * information. * * Prefer to use `Vpc.fromLookup()` instead. */ public static fromVpcAttributes(scope: Construct, id: string, attrs: VpcAttributes): IVpc { return new ImportedVpc(scope, id, attrs, false); } /** * Import an existing VPC by querying the AWS environment this stack is deployed to. * * This function only needs to be used to use VPCs not defined in your CDK * application. If you are looking to share a VPC between stacks, you can * pass the `Vpc` object between stacks and use it as normal. * * Calling this method will lead to a lookup when the CDK CLI is executed. * You can therefore not use any values that will only be available at * CloudFormation execution time (i.e., Tokens). * * The VPC information will be cached in `cdk.context.json` and the same VPC * will be used on future runs. To refresh the lookup, you will have to * evict the value from the cache using the `cdk context` command. See * https://docs.aws.amazon.com/cdk/latest/guide/context.html for more information. */ public static fromLookup(scope: Construct, id: string, options: VpcLookupOptions): IVpc { if (Token.isUnresolved(options.vpcId) || Token.isUnresolved(options.vpcName) || Object.values(options.tags || {}).some(Token.isUnresolved) || Object.keys(options.tags || {}).some(Token.isUnresolved)) { throw new ValidationError('All arguments to Vpc.fromLookup() must be concrete (no Tokens)', scope); } const filter: {[key: string]: string} = makeTagFilter(options.tags); // We give special treatment to some tags if (options.vpcId) { filter['vpc-id'] = options.vpcId; } if (options.vpcName) { filter['tag:Name'] = options.vpcName; } if (options.ownerAccountId) { filter['owner-id'] = options.ownerAccountId; } if (options.isDefault !== undefined) { filter.isDefault = options.isDefault ? 'true' : 'false'; } const overrides: {[key: string]: string} = {}; if (options.region) { overrides.region = options.region; } const attributes: cxapi.VpcContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.VPC_PROVIDER, props: { ...overrides, filter, returnAsymmetricSubnets: true, returnVpnGateways: options.returnVpnGateways, subnetGroupNameTag: options.subnetGroupNameTag, } as cxschema.VpcContextQuery, dummyValue: undefined, }).value; return new LookedUpVpc(scope, id, attributes ?? DUMMY_VPC_PROPS, attributes === undefined); /** * Prefixes all keys in the argument with `tag:`.` */ function makeTagFilter(tags: { [name: string]: string } | undefined): { [name: string]: string } { const result: { [name: string]: string } = {}; for (const [name, value] of Object.entries(tags || {})) { result[`tag:${name}`] = value; } return result; } } /** * Identifier for this VPC */ public readonly vpcId: string; /** * @attribute */ public readonly vpcArn: string; /** * @attribute */ public readonly vpcCidrBlock: string; /** * @attribute */ public readonly vpcDefaultNetworkAcl: string; /** * @attribute */ public readonly vpcCidrBlockAssociations: string[]; /** * @attribute */ public readonly vpcDefaultSecurityGroup: string; /** * @attribute */ public readonly vpcIpv6CidrBlocks: string[]; /** * List of public subnets in this VPC */ public readonly publicSubnets: ISubnet[] = []; /** * List of private subnets in this VPC */ public readonly privateSubnets: ISubnet[] = []; /** * List of isolated subnets in this VPC */ public readonly isolatedSubnets: ISubnet[] = []; /** * AZs for this VPC */ public readonly availabilityZones: string[]; /** * Internet Gateway for the VPC. Note that in case the VPC is configured only * with ISOLATED subnets, this attribute will be `undefined`. */ public readonly internetGatewayId?: string; public readonly internetConnectivityEstablished: IDependable; /** * Indicates if instances launched in this VPC will have public DNS hostnames. */ public readonly dnsHostnamesEnabled: boolean; /** * Indicates if DNS support is enabled for this VPC. */ public readonly dnsSupportEnabled: boolean; /** * The VPC resource */ private readonly resource: CfnVPC; /** * Indicates if IPv4 addresses will be used in the VPC. * * True for IPV4_ONLY and DUAL_STACK VPCs. */ private readonly useIpv4: boolean; /** * Indicates if IPv6 addresses will be used in the VPC. * * True for DUAL_STACK VPCs. False for IPV4_ONLY VPCs. */ private readonly useIpv6: boolean; /** * The provider of ipv4 addresses */ private readonly ipAddresses: IIpAddresses; /** * The provider of IPv6 addresses. */ private readonly ipv6Addresses?: IIpv6Addresses; /** * The IPv6 CIDR block CFN resource. * * Needed to create a dependency for the subnets. */ private readonly ipv6CidrBlock?: CfnVPCCidrBlock; /** * The IPv6 CIDR block string representation. */ private readonly ipv6SelectedCidr?: string; /** * Subnet configurations for this VPC */ private subnetConfiguration: SubnetConfiguration[] = []; private readonly _internetConnectivityEstablished = new DependencyGroup(); /** * Vpc creates a VPC that spans a whole region. * It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone. * Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway. * Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ). */ constructor(scope: Construct, id: string, props: VpcProps = {}) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); const stack = Stack.of(this); // Can't have enabledDnsHostnames without enableDnsSupport if (props.enableDnsHostnames && !props.enableDnsSupport) { throw new ValidationError('To use DNS Hostnames, DNS Support must be enabled, however, it was explicitly disabled.', this); } if (props.availabilityZones && props.maxAzs) { throw new ValidationError('Vpc supports \'availabilityZones\' or \'maxAzs\', but not both.', this); } const cidrBlock = ifUndefined(props.cidr, Vpc.DEFAULT_CIDR_RANGE); if (Token.isUnresolved(cidrBlock)) { throw new ValidationError('\'cidr\' property must be a concrete CIDR string, got a Token (we need to parse it for automatic subdivision)', this); } if (props.ipAddresses && props.cidr) { throw new ValidationError('supply at most one of ipAddresses or cidr', this); } const ipProtocol = props.ipProtocol ?? IpProtocol.IPV4_ONLY; // this property can be set to false if an IPv6_ONLY VPC is implemented in the future this.useIpv4 = ipProtocol === IpProtocol.IPV4_ONLY || ipProtocol === IpProtocol.DUAL_STACK; this.useIpv6 = ipProtocol === IpProtocol.DUAL_STACK; const ipv6OnlyProps: Array<keyof VpcProps> = ['ipv6Addresses']; if (!this.useIpv6) { for (const prop of ipv6OnlyProps) { if (props[prop] !== undefined) { throw new ValidationError(`${prop} can only be set if IPv6 is enabled. Set ipProtocol to DUAL_STACK`, this); } } } this.ipAddresses = props.ipAddresses ?? IpAddresses.cidr(cidrBlock); this.dnsHostnamesEnabled = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; this.dnsSupportEnabled = props.enableDnsSupport == null ? true : props.enableDnsSupport; const instanceTenancy = props.defaultInstanceTenancy || 'default'; this.internetConnectivityEstablished = this._internetConnectivityEstablished; const vpcIpAddressOptions = this.ipAddresses.allocateVpcCidr(); // Define a VPC using the provided CIDR range this.resource = new CfnVPC(this, 'Resource', { cidrBlock: vpcIpAddressOptions.cidrBlock, ipv4IpamPoolId: vpcIpAddressOptions.ipv4IpamPoolId, ipv4NetmaskLength: vpcIpAddressOptions.ipv4NetmaskLength, enableDnsHostnames: this.dnsHostnamesEnabled, enableDnsSupport: this.dnsSupportEnabled, instanceTenancy, }); this.vpcDefaultNetworkAcl = this.resource.attrDefaultNetworkAcl; this.vpcCidrBlockAssociations = this.resource.attrCidrBlockAssociations; this.vpcCidrBlock = this.resource.attrCidrBlock; this.vpcDefaultSecurityGroup = this.resource.attrDefaultSecurityGroup; this.vpcIpv6CidrBlocks = this.resource.attrIpv6CidrBlocks; Tags.of(this).add(NAME_TAG, props.vpcName || this.node.path); if (props.availabilityZones) { // If given AZs and stack AZs are both resolved, then validate their compatibility. const resolvedStackAzs = this.resolveStackAvailabilityZones(stack.availabilityZones); const areGivenAzsSubsetOfStack = resolvedStackAzs.length === 0 || props.availabilityZones.every(az => Token.isUnresolved(az) ||resolvedStackAzs.includes(az)); if (!areGivenAzsSubsetOfStack) { throw new ValidationError(`Given VPC 'availabilityZones' ${props.availabilityZones} must be a subset of the stack's availability zones ${resolvedStackAzs}`, this); } this.availabilityZones = props.availabilityZones; } else { const maxAZs = props.maxAzs ?? 3; this.availabilityZones = stack.availabilityZones.slice(0, maxAZs); } for (let i = 0; props.reservedAzs && i < props.reservedAzs; i++) { this.availabilityZones.push(FAKE_AZ_NAME); } this.vpcId = this.resource.ref; this.vpcArn = Arn.format({ service: 'ec2', resource: 'vpc', resourceName: this.vpcId, }, stack); const defaultSubnet = props.natGateways === 0 ? Vpc.DEFAULT_SUBNETS_NO_NAT : Vpc.DEFAULT_SUBNETS; this.subnetConfiguration = ifUndefined(props.subnetConfiguration, defaultSubnet); const natGatewayPlacement = props.natGatewaySubnets || { subnetType: SubnetType.PUBLIC }; const natGatewayCount = determineNatGatewayCount(props.natGateways, this.subnetConfiguration, this.availabilityZones.length); if (this.useIpv6) { this.ipv6Addresses = props.ipv6Addresses ?? Ipv6Addresses.amazonProvided(); this.ipv6CidrBlock = this.ipv6Addresses.allocateVpcIpv6Cidr({ scope: this, vpcId: this.vpcId, }); this.ipv6SelectedCidr = Fn.select(0, this.resource.attrIpv6CidrBlocks); } // subnetConfiguration must be set before calling createSubnets this.createSubnets(); const createInternetGateway = props.createInternetGateway ?? true; const allowOutbound = this.subnetConfiguration.filter( subnet => (subnet.subnetType !== SubnetType.PRIVATE_ISOLATED && subnet.subnetType !== SubnetType.ISOLATED && !subnet.reserved)).length > 0; // Create an Internet Gateway and attach it if necessary if (allowOutbound && createInternetGateway) { const igw = new CfnInternetGateway(this, 'IGW', { }); this.internetGatewayId = igw.ref; this._internetConnectivityEstablished.add(igw); const att = new CfnVPCGatewayAttachment(this, 'VPCGW', { internetGatewayId: igw.ref, vpcId: this.resource.ref, }); (this.publicSubnets as PublicSubnet[]).forEach(publicSubnet => { // configure IPv4 route if (this.useIpv4) { publicSubnet.addDefaultInternetRoute(igw.ref, att); } // configure IPv6 route if VPC is dual stack if (this.useIpv6) { publicSubnet.addIpv6DefaultInternetRoute(igw.ref); } }); // if gateways are needed create them if (natGatewayCount > 0) { const provider = props.natGatewayProvider || NatProvider.gateway(); this.createNatGateways(provider, natGatewayCount, natGatewayPlacement); } } // Create an Egress Only Internet Gateway and attach it if necessary if (this.useIpv6 && this.privateSubnets) { const eigw = new CfnEgressOnlyInternetGateway(this, 'EIGW6', { vpcId: this.vpcId, }); (this.privateSubnets as PrivateSubnet[]).forEach(privateSubnet => { privateSubnet.addIpv6DefaultEgressOnlyInternetRoute(eigw.ref); }); } if (props.vpnGateway && this.publicSubnets.length === 0 && this.privateSubnets.length === 0 && this.isolatedSubnets.length === 0) { throw new ValidationError('Can not enable the VPN gateway while the VPC has no subnets at all', this); } if ((props.vpnConnections || props.vpnGatewayAsn) && props.vpnGateway === false) { throw new ValidationError('Cannot specify `vpnConnections` or `vpnGatewayAsn` when `vpnGateway` is set to false.', this); } if (props.vpnGateway || props.vpnConnections || props.vpnGatewayAsn) { this.enableVpnGateway({ amazonSideAsn: props.vpnGatewayAsn, type: VpnConnectionType.IPSEC_1, vpnRoutePropagation: props.vpnRoutePropagation, }); const vpnConnections = props.vpnConnections || {}; for (const [connectionId, connection] of Object.entries(vpnConnections)) { this.addVpnConnection(connectionId, connection); } } // Allow creation of gateway endpoints on VPC instantiation as those can be // immediately functional without further configuration. This is not the case // for interface endpoints where the security group must be configured. if (props.gatewayEndpoints) { const gatewayEndpoints = props.gatewayEndpoints || {}; for (const [endpointId, endpoint] of Object.entries(gatewayEndpoints)) { this.addGatewayEndpoint(endpointId, endpoint); } } // Add flow logs to the VPC if (props.flowLogs) { const flowLogs = props.flowLogs || {}; for (const [flowLogId, flowLog] of Object.entries(flowLogs)) { this.addFlowLog(flowLogId, flowLog); } } const restrictFlag = FeatureFlags.of(this).isEnabled(EC2_RESTRICT_DEFAULT_SECURITY_GROUP); if ((restrictFlag && props.restrictDefaultSecurityGroup !== false) || props.restrictDefaultSecurityGroup) { this.restrictDefaultSecurityGroup(); } } /** * Adds a new S3 gateway endpoint to this VPC * * @deprecated use `addGatewayEndpoint()` instead */ @MethodMetadata() public addS3Endpoint(id: string, subnets?: SubnetSelection[]): GatewayVpcEndpoint { return new GatewayVpcEndpoint(this, id, { service: GatewayVpcEndpointAwsService.S3, vpc: this, subnets, }); } /** * Adds a new DynamoDB gateway endpoint to this VPC * * @deprecated use `addGatewayEndpoint()` instead */ @MethodMetadata() public addDynamoDbEndpoint(id: string, subnets?: SubnetSelection[]): GatewayVpcEndpoint { return new GatewayVpcEndpoint(this, id, { service: GatewayVpcEndpointAwsService.DYNAMODB, vpc: this, subnets, }); } private createNatGateways(provider: NatProvider, natCount: number, placement: SubnetSelection): void { const natSubnets: PublicSubnet[] = this.selectSubnetObjects(placement) as PublicSubnet[]; for (const sub of natSubnets) { if (this.publicSubnets.indexOf(sub) === -1) { throw new ValidationError(`natGatewayPlacement ${placement} contains non public subnet ${sub}`, this); } } provider.configureNat({ vpc: this, natSubnets: natSubnets.slice(0, natCount), privateSubnets: this.privateSubnets as PrivateSubnet[], }); } /** * createSubnets creates the subnets specified by the subnet configuration * array or creates the `DEFAULT_SUBNETS` configuration */ private createSubnets() { const requestedSubnets: RequestedSubnet[] = []; this.subnetConfiguration.forEach((configuration)=> ( this.availabilityZones.forEach((az, index) => { requestedSubnets.push({ availabilityZone: az, subnetConstructId: subnetId(configuration.name, index), configuration, }); }, ))); let { allocatedSubnets } = this.ipAddresses.allocateSubnetsCidr({ vpcCidr: this.vpcCidrBlock, requestedSubnets, }); if (allocatedSubnets.length != requestedSubnets.length) { throw new ValidationError('Incomplete Subnet Allocation; response array dose not equal input array', this); } if (this.useIpv6) { if (this.ipv6SelectedCidr === undefined) { throw new ValidationError('No IPv6 CIDR block associated with this VPC could be found', this); } if (this.ipv6Addresses === undefined) { throw new ValidationError('No IPv6 IpAddresses were found', this); } // create the IPv6 CIDR blocks const subnetIpv6Cidrs = this.ipv6Addresses.createIpv6CidrBlocks({ ipv6SelectedCidr: this.ipv6SelectedCidr, subnetCount: allocatedSubnets.length, }); // copy the list of allocated subnets while assigning the IPv6 CIDR const allocatedSubnetsIpv6 = this.ipv6Addresses.allocateSubnetsIpv6Cidr({ allocatedSubnets: allocatedSubnets, ipv6Cidrs: subnetIpv6Cidrs, }); // assign allocated subnets to list with IPv6 addresses allocatedSubnets = allocatedSubnetsIpv6.allocatedSubnets; } this.createSubnetResources(requestedSubnets, allocatedSubnets); if (this.useIpv6) { // add dependencies (this.publicSubnets as PublicSubnet[]).forEach(publicSubnet => { if (this.ipv6CidrBlock !== undefined) { publicSubnet.node.addDependency(this.ipv6CidrBlock); } }); (this.privateSubnets as PrivateSubnet[]).forEach(privateSubnet => { if (this.ipv6CidrBlock !== undefined) { privateSubnet.node.addDependency(this.ipv6CidrBlock); } }); (this.isolatedSubnets as PrivateSubnet[]).forEach((isolatedSubnet) => { if (this.ipv6CidrBlock !== undefined) { isolatedSubnet.node.addDependency(this.ipv6CidrBlock); } }); } } /** * Defaults to true in Subnet.Public for IPV4_ONLY VPCs. * * Defaults to false in Subnet.Public for DUAL_STACK VPCs. * * Always defaults to false in non-public subnets and will error if set. */ private calculateMapPublicIpOnLaunch(subnetConfig: SubnetConfiguration) { if (subnetConfig.subnetType === SubnetType.PUBLIC) { return (subnetConfig.mapPublicIpOnLaunch !== undefined) ? subnetConfig.mapPublicIpOnLaunch : !this.useIpv6; // changes default based on protocol of vpc } else { if (subnetConfig.mapPublicIpOnLaunch !== undefined) { throw new ValidationError(`${subnetConfig.subnetType} subnet cannot include mapPublicIpOnLaunch parameter`, this); } return false; } } private createSubnetResources(requestedSubnets: RequestedSubnet[], allocatedSubnets: AllocatedSubnet[]) { allocatedSubnets.forEach((allocated, i) => { const { configuration: subnetConfig, subnetConstructId, availabilityZone } = requestedSubnets[i]; if (subnetConfig.reserved === true) { // For reserved subnets, do not create any resources return; } if (availabilityZone === FAKE_AZ_NAME) { // For reserved azs, do not create any resources return; } const ipv6OnlyProps: Array<keyof SubnetConfiguration> = ['ipv6AssignAddressOnCreation']; if (!this.useIpv6) { for (const prop of ipv6OnlyProps) { if (subnetConfig[prop] !== undefined) { throw new ValidationError(`${prop} can only be set if IPv6 is enabled. Set ipProtocol to DUAL_STACK`, this); } } } const subnetProps = { availabilityZone, vpcId: this.vpcId, cidrBlock: allocated.cidr, mapPublicIpOnLaunch: this.calculateMapPublicIpOnLaunch(subnetConfig), ipv6CidrBlock: allocated.ipv6Cidr, assignIpv6AddressOnCreation: this.useIpv6 ? subnetConfig.ipv6AssignAddressOnCreation ?? true : undefined, } satisfies SubnetProps; let subnet: Subnet; switch (subnetConfig.subnetType) { case SubnetType.PUBLIC: const publicSubnet = new PublicSubnet(this, subnetConstructId, subnetProps); this.publicSubnets.push(publicSubnet); subnet = publicSubnet; break; case SubnetType.PRIVATE_WITH_EGRESS: case SubnetType.PRIVATE_WITH_NAT: case SubnetType.PRIVATE: const privateSubnet = new PrivateSubnet(this, subnetConstructId, subnetProps); this.privateSubnets.push(privateSubnet); subnet = privateSubnet; break; case SubnetType.PRIVATE_ISOLATED: case SubnetType.ISOLATED: const isolatedSubnet = new PrivateSubnet(this, subnetConstructId, subnetProps); this.isolatedSubnets.push(isolatedSubnet); subnet = isolatedSubnet; break; default: throw new ValidationError(`Unrecognized subnet type: ${subnetConfig.subnetType}`, this); } // These values will be used to recover the config upon provider import const includeResourceTypes = [CfnSubnet.CFN_RESOURCE_TYPE_NAME]; Tags.of(subnet).add(SUBNETNAME_TAG, subnetConfig.name, { includeResourceTypes }); Tags.of(subnet).add(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), { includeResourceTypes }); }); } private restrictDefaultSecurityGroup(): void { const id = 'Custom::VpcRestrictDefaultSG'; const provider = RestrictDefaultSgProvider.getOrCreateProvider(this, id, { description: 'Lambda function for removing all inbound/outbound rules from the VPC default security group', }); provider.addToRolePolicy({ Effect: 'Allow', Action: [ 'ec2:AuthorizeSecurityGroupIngress', 'ec2:AuthorizeSecurityGroupEgress', 'ec2:RevokeSecurityGroupIngress', 'ec2:RevokeSecurityGroupEgress', ], Resource: [ Stack.of(this).formatArn({ resource: 'security-group', service: 'ec2', resourceName: this.vpcDefaultSecurityGroup, }), ], }); new CustomResource(this, 'RestrictDefaultSecurityGroupCustomResource', { resourceType: id, serviceToken: provider.serviceToken, properties: { DefaultSecurityGroupId: this.vpcDefaultSecurityGroup, Account: Stack.of(this).account, }, }); } /** * Returns the list of resolved availability zones found in the provided stack availability * zones. * * Note: A resolved availability zone refers to an availability zone that is not a token * and is also not a dummy value. */ private resolveStackAvailabilityZones(stackAvailabilityZones: string[]): string[] { const dummyValues = ['dummy1a', 'dummy1b', 'dummy1c']; // if an az is resolved and it is not a 'dummy' value, then add it to array as a resolved az return stackAvailabilityZones.filter(az => !Token.isUnresolved(az) && !dummyValues.includes(az)); } } const SUBNETTYPE_TAG = 'aws-cdk:subnet-type'; const SUBNETNAME_TAG = 'aws-cdk:subnet-name'; function subnetTypeTagValue(type: SubnetType) { switch (type) { case SubnetType.PUBLIC: return 'Public'; case SubnetType.PRIVATE_WITH_EGRESS: case SubnetType.PRIVATE_WITH_NAT: case SubnetType.PRIVATE: return 'Private'; case SubnetType.PRIVATE_ISOLATED: case SubnetType.ISOLATED: return 'Isolated'; } } /** * Specify configuration parameters for a VPC subnet */ export interface SubnetProps { /** * The availability zone for the subnet */ readonly availabilityZone: string; /** * The VPC which this subnet is part of */ readonly vpcId: string; /** * The CIDR notation for this subnet */ readonly cidrBlock: string; /** * Controls if a public IP is associated to an instance at launch * * @default true in Subnet.Public, false in Subnet.Private or Subnet.Isolated. */ readonly mapPublicIpOnLaunch?: boolean; /** * The IPv6 CIDR block. * * If you specify AssignIpv6AddressOnCreation, you must also specify Ipv6CidrBlock. * * @default - no IPv6 CIDR block. */ readonly ipv6CidrBlock?: string; /** * Indicates whether a network interface created in this subnet receives an IPv6 address. * * If you specify AssignIpv6AddressOnCreation, you must also specify Ipv6CidrBlock. * * @default false */ readonly assignIpv6AddressOnCreation?: boolean; } /** * Represents a new VPC subnet resource * * @resource AWS::EC2::Subnet */ export class Subnet extends Resource implements ISubnet { public static isVpcSubnet(x: any): x is Subnet { return VPC_SUBNET_SYMBOL in x; } public static fromSubnetAttributes(scope: Construct, id: string, attrs: SubnetAttributes): ISubnet { return new ImportedSubnet(scope, id, attrs); } /** * Import existing subnet from id. */ // eslint-disable-next-line @typescript-eslint/no-shadow public static fromSubnetId(scope: Construct, id: string, subnetId: string): ISubnet { return this.fromSubnetAttributes(scope, id, { subnetId }); } /** * The Availability Zone the subnet is located in */ public readonly availabilityZone: string; /** * @attribute */ public readonly ipv4CidrBlock: string; /** * The subnetId for this particular subnet */ public readonly subnetId: string; /** * @attribute */ public readonly subnetVpcId: string; /** * @attribute */ public readonly subnetAvailabilityZone: string; /** * @attribute */ public readonly subnetIpv6CidrBlocks: string[]; /** * The Amazon Resource Name (ARN) of the Outpost for this subnet (if one exists). * @attribute */ public readonly subnetOutpostArn: string; /** * @attribute */ public readonly subnetNetworkAclAssociationId: string; /** * Parts of this VPC subnet */ public readonly dependencyElements: IDependable[] = []; /** * The routeTableId attached to this subnet. */ public readonly routeTable: IRouteTable; public readonly internetConnectivityEstablished: IDependable; private readonly _internetConnectivityEstablished = new DependencyGroup(); private _networkAcl: INetworkAcl; constructor(scope: Construct, id: string, props: SubnetProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); Object.defineProperty(this, VPC_SUBNET_SYMBOL, { value: true }); 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, availabilityZone: props.availabilityZone, mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, ipv6CidrBlock: props.ipv6CidrBlock, assignIpv6AddressOnCreation: props.assignIpv6AddressOnCreation, }); this.subnetId = subnet.ref; this.subnetVpcId = subnet.attrVpcId; this.subnetAvailabilityZone = subnet.attrAvailabilityZone; this.subnetIpv6CidrBlocks = subnet.attrIpv6CidrBlocks; this.subnetOutpostArn = subnet.attrOutpostArn; // subnet.attrNetworkAclAssociationId is the default ACL after the subnet // was just created. However, the ACL can be replaced at a later time. this._networkAcl = NetworkAcl.fromNetworkAclId(this, 'Acl', subnet.attrNetworkAclAssociationId); this.subnetNetworkAclAssociationId = Lazy.string({ produce: () => this._networkAcl.networkAclId }); this.node.defaultChild = subnet; const table = new CfnRouteTable(this, 'RouteTable', { vpcId: props.vpcId, }); this.routeTable = { routeTableId: table.ref }; // Associate the public route table for this subnet, to this subnet const routeAssoc = new CfnSubnetRouteTableAssociation(this, 'RouteTableAssociation', { subnetId: this.subnetId, routeTableId: table.ref, }); this._internetConnectivityEstablished.add(routeAssoc); this.internetConnectivityEstablished = this._internetConnectivityEstablished; } /** * Create a default route that points to a passed IGW, with a dependency * on the IGW's attachment to the VPC. * * @param gatewayId the logical ID (ref) of the gateway attached to your VPC * @param gatewayAttachment the gateway attachment construct to be added as a dependency */ @MethodMetadata() public addDefaultInternetRoute(gatewayId: string, gatewayAttachment: IDependable) { const route = new CfnRoute(this, 'DefaultRoute', { routeTableId: this.routeTable.routeTableId, destinationCidrBlock: '0.0.0.0/0', gatewayId, }); route.node.addDependency(gatewayAttachment); // Since the 'route' depends on the gateway attachment, just // depending on the route is enough. this._internetConnectivityEstablished.add(route); } /** * Create a default IPv6 route that points to a passed IGW. * * @param gatewayId the logical ID (ref) of the gateway attached to your VPC */ @MethodMetadata() public addIpv6DefaultInternetRoute(gatewayId: string) { this.addRoute('DefaultRoute6', { routerType: RouterType.GATEWAY, routerId: gatewayId, destinationIpv6CidrBlock: '::/0', enablesInternetConnectivity: true, }); } /** * Create a default IPv6 route that points to a passed EIGW. * * @param gatewayId the logical ID (ref) of the gateway attached to your VPC */ @MethodMetadata() public addIpv6DefaultEgressOnlyInternetRoute(gatewayId: string) { this.addRoute('DefaultRoute6', { routerType: RouterType.EGRESS_ONLY_INTERNET_GATEWAY, routerId: gatewayId, destinationIpv6CidrBlock: '::/0', enablesInternetConnectivity: true, }); } /** * Network ACL associated with this Subnet * * Upon creation, this is the default ACL which allows all traffic, except * explicit DENY entries that you add. * * You can replace it with a custom ACL which denies all traffic except * the explicit ALLOW entries that you add by creating a `NetworkAcl` * object and calling `associateNetworkAcl()`. */ public get networkAcl(): INetworkAcl { return this._networkAcl; } /** * Adds an entry to this subnets route table that points to the passed NATGatewayId * @param natGatewayId The ID of the NAT gateway */ @MethodMetadata() public addDefaultNatRoute(natGatewayId: string) { this.addRoute('DefaultRoute', { routerType: RouterType.NAT_GATEWAY, routerId: natGatewayId, enablesInternetConnectivity: true, }); } /** * Adds an entry to this subnets route table that points to the passed NATGatewayId. * Uses the known 64:ff9b::/96 prefix. * @param natGatewayId The ID of the NAT gateway */ @MethodMetadata() public addIpv6Nat64Route(natGatewayId: string) { this.addRoute('Nat64', { routerType: RouterType.NAT_GATEWAY, routerId: natGatewayId, enablesInternetConnectivity: true, destinationIpv6CidrBlock: '64:ff9b::/96', }); } /** * Adds an entry to this subnets route table */ @MethodMetadata() public addRoute(id: string, options: AddRouteOptions) { if (options.destinationCidrBlock && options.destinationIpv6CidrBlock) { throw new ValidationError('Cannot specify both \'destinationCidrBlock\' and \'destinationIpv6CidrBlock\'', this); } const route = new CfnRoute(this, id, { routeTableId: this.routeTable.routeTableId, destinationCidrBlock: options.destinationCidrBlock || (options.destinationIpv6CidrBlock === undefined ? '0.0.0.0/0' : undefined), destinationIpv6CidrBlock: options.destinationIpv6CidrBlock, [routerTypeToPropName(options.routerType)]: options.routerId, }); if (options.enablesInternetConnectivity) { this._internetConnectivityEstablished.add(route); } } @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, }); } } /** * Options for adding a new route to a subnet */ export interface AddRouteOptions { /** * IPv4 range this route applies to * * @default '0.0.0.0/0' */ readonly destinationCidrBlock?: string; /** * IPv6 range this route applies to * * @default - Uses IPv6 */ readonly destinationIpv6CidrBlock?: string; /** * What type of router to route this traffic to */ readonly routerType: RouterType; /** * The ID of the router * * Can be an instance ID, gateway ID, etc, depending on the router type. */ readonly routerId: string; /** * Whether this route will enable internet connectivity * * If true, this route will be added before any AWS resources that depend * on internet connectivity in the VPC will be created. * * @default false */ readonly enablesInternetConnectivity?: boolean; } /** * Type of router used in route */ export enum RouterType { /** * Carrier gateway */ CARRIER_GATEWAY = 'CarrierGateway', /** * Egress-only Internet Gateway */ EGRESS_ONLY_INTERNET_GATEWAY = 'EgressOnlyInternetGateway', /** * Internet Gateway */ GATEWAY = 'Gateway', /** * Instance */ INSTANCE = 'Instance', /** * Local Gateway */ LOCAL_GATEWAY = 'LocalGateway', /** * NAT Gateway */ NAT_GATEWAY = 'NatGateway', /** * Network Interface */ NETWORK_INTERFACE = 'NetworkInterface', /** * Transit Gateway */ TRANSIT_GATEWAY = 'TransitGateway', /** * VPC peering connection */ VPC_PEERING_CONNECTION = 'VpcPeeringConnection', /** * VPC Endpoint for gateway load balancers */ VPC_ENDPOINT = 'VpcEndpoint', } function routerTypeToPropName(routerType: RouterType) { return ({ [RouterType.CARRIER_GATEWAY]: 'carrierGatewayId', [RouterType.EGRESS_ONLY_INTERNET_GATEWAY]: 'egressOnlyInternetGatewayId', [RouterType.GATEWAY]: 'gatewayId', [RouterType.INSTANCE]: 'instanceId', [RouterType.LOCAL_GATEWAY]: 'localGatewayId', [RouterType.NAT_GATEWAY]: 'natGatewayId', [RouterType.NETWORK_INTERFACE]: 'networkInterfaceId', [RouterType.TRANSIT_GATEWAY]: 'transitGatewayId', [RouterType.VPC_PEERING_CONNECTION]: 'vpcPeeringConnectionId', [RouterType.VPC_ENDPOINT]: 'vpcEndpointId', })[routerType]; } export interface PublicSubnetProps extends SubnetProps { } export interface IPublicSubnet extends ISubnet { } export interface PublicSubnetAttributes extends SubnetAttributes { } /** * Represents a public VPC subnet resource */ export class PublicSubnet extends Subnet implements IPublicSubnet { public static fromPublicSubnetAttributes(scope: Construct, id: string, attrs: PublicSubnetAttributes): IPublicSubnet { return new ImportedSubnet(scope, id, attrs); } constructor(scope: Construct, id: string, props: PublicSubnetProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); } /** * Creates a new managed NAT gateway attached to this public subnet. * Also adds the EIP for the managed NAT. * @returns A ref to the the NAT Gateway ID */ @MethodMetadata() public addNatGateway(eipAllocationId?: string) { // Create a NAT Gateway in this public subnet const ngw = new CfnNatGateway(this, 'NATGateway', { subnetId: this.subnetId, allocationId: eipAllocationId ?? new CfnEIP(this, 'EIP', { domain: 'vpc', }).attrAllocationId, }); ngw.node.addDependency(this.internetConnectivityEstablished); return ngw; } } export interface PrivateSubnetProps extends SubnetProps { } export interface IPrivateSubnet extends ISubnet { } export interface PrivateSubnetAttributes extends SubnetAttributes { } /** * Represents a private VPC subnet resource */ export class PrivateSubnet extends Subnet implements IPrivateSubnet { public static fromPrivateSubnetAttributes(scope: Construct, id: string, attrs: PrivateSubnetAttributes): IPrivateSubnet { return new ImportedSubnet(scope, id, attrs); } constructor(scope: Construct, id: string, props: PrivateSubnetProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); } } function ifUndefined<T>(value: T | undefined, defaultValue: T): T { return value ?? defaultValue; } class ImportedVpc extends VpcBase { public readonly vpcId: string; public readonly vpcArn: string; public readonly publicSubnets: ISubnet[]; public readonly privateSubnets: ISubnet[]; public readonly isolatedSubnets: ISubnet[]; public readonly availabilityZones: string[]; public readonly internetConnectivityEstablished: IDependable = new DependencyGroup(); private readonly cidr?: string | undefined; constructor(scope: Construct, id: string, props: VpcAttributes, isIncomplete: boolean) { super(scope, id, { region: props.region, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.vpcId = props.vpcId; this.vpcArn = Arn.format({ service: 'ec2', resource: 'vpc', resourceName: this.vpcId, }, Stack.of(this)); this.cidr = props.vpcCidrBlock; this.availabilityZones = props.availabilityZones; this._vpnGatewayId = props.vpnGatewayId; this.incompleteSubnetDefinition = isIncomplete; // None of the values may be unresolved list tokens for (const k of Object.keys(props) as Array<keyof VpcAttributes>) { if (Array.isArray(props[k]) && Token.isUnresolved(props[k])) { Annotations.of(this).addWarningV2(`@aws-cdk/aws-ec2:vpcAttributeIsListToken${k}`, `fromVpcAttributes: '${k}' is a list token: the imported VPC will not work with constructs that require a list of subnets at synthesis time. Use 'Vpc.fromLookup()' or 'Fn.importListValue' instead.`); } } /* eslint-disable max-len */ const pub = new ImportSubnetGroup(props.publicSubnetIds, props.publicSubnetNames, props.publicSubnetRouteTableIds, props.publicSubnetIpv4CidrBlocks, SubnetType.PUBLIC, this.availabilityZones, 'publicSubnetIds', 'publicSubnetNames', 'publicSubnetRouteTableIds', 'publicSubnetIpv4CidrBlocks'); const priv = new ImportSubnetGroup(props.privateSubnetIds, props.privateSubnetNames, props.privateSubnetRouteTableIds, props.privateSubnetIpv4CidrBlocks, SubnetType.PRIVATE_WITH_EGRESS, this.availabilityZones, 'privateSubnetIds', 'privateSubnetNames', 'privateSubnetRouteTableIds', 'privateSubnetIpv4CidrBlocks'); const iso = new ImportSubnetGroup(props.isolatedSubnetIds, props.isolatedSubnetNames, props.isolatedSubnetRouteTableIds, props.isolatedSubnetIpv4CidrBlocks, SubnetType.PRIVATE_ISOLATED, this.availabilityZones, 'isolatedSubnetIds', 'isolatedSubnetNames', 'isolatedSubnetRouteTableIds', 'isolatedSubnetIpv4CidrBlocks'); /* eslint-enable max-len */ this.publicSubnets = pub.import(this); this.privateSubnets = priv.import(this); this.isolatedSubnets = iso.import(this); } public get vpcCidrBlock(): string { if (this.cidr === undefined) { throw new ValidationError('Cannot perform this operation: \'vpcCidrBlock\' was not supplied when creating this VPC', this); } return this.cidr; } } class LookedUpVpc extends VpcBase { public readonly vpcId: string; public readonly vpcArn: string; public readonly internetConnectivityEstablished: IDependable = new DependencyGroup(); public readonly availabilityZones: string[]; public readonly publicSubnets: ISubnet[]; public readonly privateSubnets: ISubnet[]; public readonly isolatedSubnets: ISubnet[]; private readonly cidr?: string | undefined; constructor(scope: Construct, id: string, props: cxapi.VpcContextResponse, isIncomplete: boolean) { super(scope, id, { region: props.region, account: props.ownerAccountId, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.vpcId = props.vpcId; this.vpcArn = Arn.format({ service: 'ec2', resource: 'vpc', resourceName: this.vpcId, region: this.env.region, account: this.env.account, }, Stack.of(this)); this.cidr = props.vpcCidrBlock; this._vpnGatewayId = props.vpnGatewayId; this.incompleteSubnetDefinition = isIncomplete; const subnetGroups = props.subnetGroups || []; const availabilityZones = Array.from(new Set<string>(flatMap(subnetGroups, subnetGroup => { return subnetGroup.subnets.map(subnet => subnet.availabilityZone); }))); availabilityZones.sort((az1, az2) => az1.localeCompare(az2)); this.availabilityZones = availabilityZones; this.publicSubnets = this.extractSubnetsOfType(subnetGroups, cxapi.VpcSubnetGroupType.PUBLIC); this.privateSubnets = this.extractSubnetsOfType(subnetGroups, cxapi.VpcSubnetGroupType.PRIVATE); this.isolatedSubnets = this.extractSubnetsOfType(subnetGroups, cxapi.VpcSubnetGroupType.ISOLATED); } public get vpcCidrBlock(): string { if (this.cidr === undefined) { // Value might be cached from an old CLI version, so bumping the CX API protocol to // force the value to exist would not have helped. throw new ValidationError('Cannot perform this operation: \'vpcCidrBlock\' was not found when looking up this VPC. Use a newer version of the CDK CLI and clear the old context value.', this); } return this.cidr; } private extractSubnetsOfType(subnetGroups: cxapi.VpcSubnetGroup[], subnetGroupType: cxapi.VpcSubnetGroupType): ISubnet[] { return flatMap(subnetGroups.filter(subnetGroup => subnetGroup.type === subnetGroupType), subnetGroup => this.subnetGroupToSubnets(subnetGroup)); } private subnetGroupToSubnets(subnetGroup: cxapi.VpcSubnetGroup): ISubnet[] { const ret = new Array<ISubnet>(); for (let i = 0; i < subnetGroup.subnets.length; i++) { const vpcSubnet = subnetGroup.subnets[i]; ret.push(Subnet.fromSubnetAttributes(this, `${subnetGroup.name}Subnet${i + 1}`, { availabilityZone: vpcSubnet.availabilityZone, subnetId: vpcSubnet.subnetId, routeTableId: vpcSubnet.routeTableId, ipv4CidrBlock: vpcSubnet.cidr, })); } return ret; } } function flatMap<T, U>(xs: T[], fn: (x: T) => U[]): U[] { const ret = new Array<U>(); for (const x of xs) { ret.push(...fn(x)); } return ret; } class CompositeDependable implements IDependable { private readonly dependables = new Array<IDependable>(); constructor() { const self = this; Dependable.implement(this, { get dependencyRoots() { const ret = new Array<IConstruct>(); for (const dep of self.dependables) { ret.push(...Dependable.of(dep).dependencyRoots); } return ret; }, }); } /** * Add a construct to the dependency roots */ public add(dep: IDependable) { this.dependables.push(dep); } } /** * Invoke a function on a value (for its side effect) and return the value */ function tap<T>(x: T, fn: (x: T) => void): T { fn(x); return x; } class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivateSubnet { public readonly internetConnectivityEstablished: IDependable = new DependencyGroup(); 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); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, attrs); if (!attrs.routeTableId) { // The following looks a little weird, but comes down to: // // * Is the subnetId itself unresolved ({ Ref: Subnet }); or // * Was it the accidentally extracted first element of a list-encoded // token? ({ Fn::ImportValue: Subnets } => ['#{Token[1234]}'] => // '#{Token[1234]}' // // There's no other API to test for the second case than to the string back into // a list and see if the combination is Unresolved. // // In both cases we can't output the subnetId literally into the metadata (because it'll // be useless). In the 2nd case even, if we output it to metadata, the `resolve()` call // that gets done on the metadata will even `throw`, because the '#{Token}' value will // occur in an illegal position (not in a list context). const ref = Token.isUnresolved(attrs.subnetId) || Token.isUnresolved([attrs.subnetId]) ? `at '${Node.of(scope).path}/${id}'` : `'${attrs.subnetId}'`; // eslint-disable-next-line max-len Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:noSubnetRouteTableId', `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 = { // Forcing routeTableId to pretend non-null to maintain backwards-compatibility. See https://github.com/aws/aws-cdk/pull/3171 routeTableId: attrs.routeTableId!, }; } public get availabilityZone(): string { if (!this._availabilityZone) { // eslint-disable-next-line max-len throw new ValidationError('You cannot reference a Subnet\'s availability zone if it was not supplied. Add the availabilityZone when importing using Subnet.fromSubnetAttributes()', this); } return this._availabilityZone; } public get ipv4CidrBlock(): string { if (!this._ipv4CidrBlock) { // tslint:disable-next-line: max-line-length throw new ValidationError('You cannot reference an imported Subnet\'s IPv4 CIDR if it was not supplied. Add the ipv4CidrBlock when importing using Subnet.fromSubnetAttributes()', this); } return this._ipv4CidrBlock; } @MethodMetadata() public associateNetworkAcl(id: string, networkAcl: INetworkAcl): void { 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, }); } } /** * Determine (and validate) the NAT gateway count w.r.t. the rest of the subnet configuration * * We have the following requirements: * * - NatGatewayCount = 0 ==> there are no private subnets * - NatGatewayCount > 0 ==> there must be public subnets * * Do we want to require that there are private subnets if there are NatGateways? * They seem pointless but I see no reason to prevent it. */ function determineNatGatewayCount(requestedCount: number | undefined, subnetConfig: SubnetConfiguration[], azCount: number) { const hasPrivateSubnets = subnetConfig.some(c => (c.subnetType === SubnetType.PRIVATE_WITH_EGRESS || c.subnetType === SubnetType.PRIVATE || c.subnetType === SubnetType.PRIVATE_WITH_NAT) && !c.reserved); const hasPublicSubnets = subnetConfig.some(c => c.subnetType === SubnetType.PUBLIC && !c.reserved); const hasCustomEgress = subnetConfig.some(c => c.subnetType === SubnetType.PRIVATE_WITH_EGRESS); const count = requestedCount !== undefined ? Math.min(requestedCount, azCount) : (hasPrivateSubnets ? azCount : 0); if (count === 0 && hasPrivateSubnets && !hasCustomEgress) { // eslint-disable-next-line max-len throw new UnscopedValidationError('If you do not want NAT gateways (natGateways=0), make sure you don\'t configure any PRIVATE(_WITH_NAT) subnets in \'subnetConfiguration\' (make them PUBLIC or ISOLATED instead)'); } if (count > 0 && !hasPublicSubnets) { // eslint-disable-next-line max-len throw new UnscopedValidationError(`If you configure PRIVATE subnets in 'subnetConfiguration', you must also configure PUBLIC subnets to put the NAT gateways into (got ${JSON.stringify(subnetConfig)}.`); } return count; } /** * There are returned when the provider has not supplied props yet * * It's only used for testing and on the first run-through. */ const DUMMY_VPC_PROPS: cxapi.VpcContextResponse = { availabilityZones: [], vpcCidrBlock: '1.2.3.4/5', isolatedSubnetIds: undefined, isolatedSubnetNames: undefined, isolatedSubnetRouteTableIds: undefined, privateSubnetIds: undefined, privateSubnetNames: undefined, privateSubnetRouteTableIds: undefined, publicSubnetIds: undefined, publicSubnetNames: undefined, publicSubnetRouteTableIds: undefined, subnetGroups: [ { name: 'Public', type: cxapi.VpcSubnetGroupType.PUBLIC, subnets: [ { availabilityZone: 'dummy1a', subnetId: 's-12345', routeTableId: 'rtb-12345s', cidr: '1.2.3.4/5', }, { availabilityZone: 'dummy1b', subnetId: 's-67890', routeTableId: 'rtb-67890s', cidr: '1.2.3.4/5', }, ], }, { name: 'Private', type: cxapi.VpcSubnetGroupType.PRIVATE, subnets: [ { availabilityZone: 'dummy1a', subnetId: 'p-12345', routeTableId: 'rtb-12345p', cidr: '1.2.3.4/5', }, { availabilityZone: 'dummy1b', subnetId: 'p-67890', routeTableId: 'rtb-57890p', cidr: '1.2.3.4/5', }, ], }, { name: 'Isolated', type: cxapi.VpcSubnetGroupType.ISOLATED, subnets: [ { availabilityZone: 'dummy1a', subnetId: 'p-12345', routeTableId: 'rtb-12345p', cidr: '1.2.3.4/5', }, { availabilityZone: 'dummy1b', subnetId: 'p-67890', routeTableId: 'rtb-57890p', cidr: '1.2.3.4/5', }, ], }, ], vpcId: 'vpc-12345', };