import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; /** * Subnet CIDR configuration for each availability zone */ export interface SubnetCidrConfig { /** CIDR for the public subnet */ publicCidr: string; /** CIDR for the primary private subnet */ private1Cidr: string; /** CIDR for the secondary private subnet (with NACL) */ private2Cidr?: string; } /** * Configuration for VPC tagging */ export interface SpicyVpcTags { owner: string; product: string; component: string; build?: string; } /** * Configuration for VPC Interface Endpoints */ export interface VpcInterfaceEndpointConfig { /** ECR API endpoint (for image metadata) @default true */ readonly ecr?: boolean; /** ECR Docker endpoint (for image layer pulls) @default true */ readonly ecrDocker?: boolean; /** CloudWatch Logs endpoint @default true */ readonly cloudwatchLogs?: boolean; /** STS endpoint (for IAM role assumption) @default true */ readonly sts?: boolean; /** Secrets Manager endpoint @default true */ readonly secretsManager?: boolean; /** SSM endpoint (for Parameter Store, Session Manager) @default true */ readonly ssm?: boolean; } /** * Properties for the SpicyVpc construct */ export interface SpicyVpcProps { /** * The CIDR block for the VPC * @default "172.1.0.0/16" */ readonly vpcCidr?: string; /** * The tenancy of instances launched into the VPC * @default "default" */ readonly vpcTenancy?: 'default' | 'dedicated'; /** * List of availability zones to use * If not provided, will use the first N AZs in the region based on numberOfAzs */ readonly availabilityZones?: string[]; /** * Number of availability zones to use (2, 3, or 4) * @default 4 */ readonly numberOfAzs?: 2 | 3 | 4; /** * Whether to create private subnets * @default true */ readonly createPrivateSubnets?: boolean; /** * Whether to create additional private subnets with dedicated NACLs * Only applies if createPrivateSubnets is true * @default true */ readonly createAdditionalPrivateSubnets?: boolean; /** * Subnet CIDR configurations per AZ * Keys should be 'a', 'b', 'c', 'd' for each AZ */ readonly subnetCidrs?: { a?: SubnetCidrConfig; b?: SubnetCidrConfig; c?: SubnetCidrConfig; d?: SubnetCidrConfig; }; /** * Tag to add to public subnets (Key=Value format) * @default "Network=Public" */ readonly publicSubnetTag?: string; /** * Tag to add to primary private subnets (Key=Value format) * @default "Network=Private" */ readonly privateSubnetATag?: string; /** * Tag to add to secondary private subnets with NACLs (Key=Value format) * @default "Network=Private" */ readonly privateSubnetBTag?: string; /** * Required tags for the VPC and resources */ readonly tags: SpicyVpcTags; /** * Whether to create VPC endpoints for AWS services * @default true */ readonly createVpcEndpoints?: boolean; /** * Enable IPv6 dual-stack with Egress-Only Internet Gateway. * Adds Amazon-provided IPv6 CIDR, assigns /64 to each subnet, * and routes private subnet IPv6 egress through EOIGW (free, outbound-only). * @default true */ readonly enableIpv6?: boolean; /** * Configuration for VPC Interface Endpoints. * Only applies when createVpcEndpoints is true. * Each endpoint costs ~$0.01/hr but eliminates NAT data processing costs for that service. * @default All endpoints enabled (ECR, ECR Docker, CloudWatch Logs, STS, Secrets Manager, SSM) */ readonly interfaceEndpoints?: VpcInterfaceEndpointConfig; } /** * Default CIDR configurations matching the original CloudFormation template */ const DEFAULT_SUBNET_CIDRS: Required = { a: { publicCidr: '172.1.128.0/20', private1Cidr: '172.1.0.0/19', private2Cidr: '172.1.192.0/21', }, b: { publicCidr: '172.1.144.0/20', private1Cidr: '172.1.32.0/19', private2Cidr: '172.1.200.0/21', }, c: { publicCidr: '172.1.160.0/20', private1Cidr: '172.1.64.0/19', private2Cidr: '172.1.208.0/21', }, d: { publicCidr: '172.1.176.0/20', private1Cidr: '172.1.96.0/19', private2Cidr: '172.1.216.0/21', }, }; /** * SpicyVpc - A multi-AZ VPC construct with public and private subnets, * NAT gateways, and optional VPC endpoints. * * This construct mirrors the functionality of the original CloudFormation * template with explicit logical IDs for idempotent deployments. */ export class SpicyVpc extends Construct { /** The VPC created by this construct */ public readonly vpc: ec2.IVpc; /** Public subnets indexed by AZ letter (a, b, c, d) */ public readonly publicSubnets: Map = new Map(); /** Primary private subnets indexed by AZ letter */ public readonly privateSubnets1: Map = new Map(); /** Secondary private subnets (with NACLs) indexed by AZ letter */ public readonly privateSubnets2: Map = new Map(); /** NAT Gateway EIPs indexed by AZ letter */ public readonly natEips: Map = new Map(); /** S3 Gateway Endpoint */ public readonly s3Endpoint?: ec2.GatewayVpcEndpoint; /** Egress-Only Internet Gateway (IPv6 outbound from private subnets) */ public readonly egressOnlyInternetGateway?: ec2.CfnEgressOnlyInternetGateway; constructor(scope: Construct, id: string, props: SpicyVpcProps) { super(scope, id); const vpcCidr = props.vpcCidr ?? '172.1.0.0/16'; const numberOfAzs = props.numberOfAzs ?? 4; const createPrivateSubnets = props.createPrivateSubnets ?? true; const createAdditionalPrivateSubnets = props.createAdditionalPrivateSubnets ?? true; const createVpcEndpoints = props.createVpcEndpoints ?? true; // Merge provided CIDRs with defaults const subnetCidrs = { ...DEFAULT_SUBNET_CIDRS, ...props.subnetCidrs, }; // Parse subnet tags const publicSubnetTagParsed = this.parseTag(props.publicSubnetTag ?? 'Network=Public'); const privateSubnetATagParsed = this.parseTag(props.privateSubnetATag ?? 'Network=Private'); const privateSubnetBTagParsed = this.parseTag(props.privateSubnetBTag ?? 'Network=Private'); // Create the VPC using L1 constructs with explicit logical IDs const cfnVpc = new ec2.CfnVPC(this, 'VPC', { cidrBlock: vpcCidr, enableDnsHostnames: true, enableDnsSupport: true, instanceTenancy: props.vpcTenancy ?? 'default', tags: [ { key: 'Name', value: cdk.Stack.of(this).stackName }, { key: 'Owner', value: props.tags.owner }, { key: 'Product', value: props.tags.product }, { key: 'Component', value: props.tags.component }, ...(props.tags.build ? [{ key: 'Build', value: props.tags.build }] : []), ], }); ((cfnVpc.node.defaultChild as cdk.CfnResource) ?? cfnVpc).overrideLogicalId('VPC'); // Create Internet Gateway const igw = new ec2.CfnInternetGateway(this, 'InternetGateway', { tags: [{ key: 'Name', value: cdk.Stack.of(this).stackName }], }); ((igw.node.defaultChild as cdk.CfnResource) ?? igw).overrideLogicalId('InternetGateway'); const vpcGatewayAttachment = new ec2.CfnVPCGatewayAttachment(this, 'VPCGatewayAttachment', { vpcId: cfnVpc.ref, internetGatewayId: igw.ref, }); ((vpcGatewayAttachment.node.defaultChild as cdk.CfnResource) ?? vpcGatewayAttachment).overrideLogicalId( 'VPCGatewayAttachment' ); // DHCP Options const dhcpOptions = new ec2.CfnDHCPOptions(this, 'DHCPOptions', { domainName: cdk.Stack.of(this).region === 'ca-central-1' ? 'ec2.internal' : `${cdk.Stack.of(this).region}.compute.internal`, domainNameServers: ['AmazonProvidedDNS'], }); ((dhcpOptions.node.defaultChild as cdk.CfnResource) ?? dhcpOptions).overrideLogicalId('DHCPOptions'); const dhcpAssociation = new ec2.CfnVPCDHCPOptionsAssociation(this, 'VPCDHCPOptionsAssociation', { vpcId: cfnVpc.ref, dhcpOptionsId: dhcpOptions.ref, }); ((dhcpAssociation.node.defaultChild as cdk.CfnResource) ?? dhcpAssociation).overrideLogicalId( 'VPCDHCPOptionsAssociation' ); // IPv6 Dual-Stack Configuration const enableIpv6 = props.enableIpv6 ?? true; let ipv6CidrBlock: ec2.CfnVPCCidrBlock | undefined; let egressOnlyIgw: ec2.CfnEgressOnlyInternetGateway | undefined; if (enableIpv6) { // Add Amazon-provided IPv6 /56 CIDR block to the VPC ipv6CidrBlock = new ec2.CfnVPCCidrBlock(this, 'IPv6CidrBlock', { vpcId: cfnVpc.ref, amazonProvidedIpv6CidrBlock: true, }); ((ipv6CidrBlock.node.defaultChild as cdk.CfnResource) ?? ipv6CidrBlock).overrideLogicalId('IPv6CidrBlock'); // Egress-Only Internet Gateway (outbound IPv6 only, drops unsolicited inbound) egressOnlyIgw = new ec2.CfnEgressOnlyInternetGateway(this, 'EgressOnlyInternetGateway', { vpcId: cfnVpc.ref, }); ((egressOnlyIgw.node.defaultChild as cdk.CfnResource) ?? egressOnlyIgw).overrideLogicalId( 'EgressOnlyInternetGateway' ); this.egressOnlyInternetGateway = egressOnlyIgw; } // Public Route Table (shared across all public subnets) const publicRouteTable = new ec2.CfnRouteTable(this, 'PublicSubnetRouteTable', { vpcId: cfnVpc.ref, tags: [ { key: 'Name', value: 'Public Subnets' }, { key: 'Network', value: 'Public' }, ], }); ((publicRouteTable.node.defaultChild as cdk.CfnResource) ?? publicRouteTable).overrideLogicalId( 'PublicSubnetRouteTable' ); const publicRoute = new ec2.CfnRoute(this, 'PublicSubnetRoute', { routeTableId: publicRouteTable.ref, destinationCidrBlock: '0.0.0.0/0', gatewayId: igw.ref, }); ((publicRoute.node.defaultChild as cdk.CfnResource) ?? publicRoute).overrideLogicalId('PublicSubnetRoute'); // Public subnet IPv6 route via IGW if (enableIpv6) { const publicIpv6Route = new ec2.CfnRoute(this, 'PublicSubnetIpv6Route', { routeTableId: publicRouteTable.ref, destinationIpv6CidrBlock: '::/0', gatewayId: igw.ref, }); ((publicIpv6Route.node.defaultChild as cdk.CfnResource) ?? publicIpv6Route).overrideLogicalId( 'PublicSubnetIpv6Route' ); } // Get AZ letters based on numberOfAzs const azLetters = ['a', 'b', 'c', 'd'].slice(0, numberOfAzs); // Determine availability zones const availabilityZones = props.availabilityZones ?? azLetters.map((letter) => `${cdk.Stack.of(this).region}${letter}`); // Track route tables for VPC endpoints const privateRouteTables: ec2.CfnRouteTable[] = []; // Track private subnet 1 route tables for VPC import const privateSubnet1RouteTables: ec2.CfnRouteTable[] = []; // Create subnets for each AZ azLetters.forEach((azLetter, index) => { const azName = availabilityZones[index]; const azUpper = azLetter.toUpperCase(); const cidrs = subnetCidrs[azLetter as keyof typeof subnetCidrs]!; // Public Subnet const publicSubnet = new ec2.CfnSubnet(this, `PublicSubnet${azUpper}`, { vpcId: cfnVpc.ref, cidrBlock: cidrs.publicCidr, availabilityZone: azName, mapPublicIpOnLaunch: true, tags: [ { key: 'Name', value: `Public Subnet ${azUpper}` }, ...(publicSubnetTagParsed ? [{ key: publicSubnetTagParsed.key, value: publicSubnetTagParsed.value }] : []), ], }); ((publicSubnet.node.defaultChild as cdk.CfnResource) ?? publicSubnet).overrideLogicalId(`PublicSubnet${azUpper}`); // Assign IPv6 /64 to public subnet if (enableIpv6 && ipv6CidrBlock) { const publicIpv6Cidr = cdk.Fn.select( index, cdk.Fn.cidr(cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks), 256, '64') ); publicSubnet.ipv6CidrBlock = publicIpv6Cidr; publicSubnet.assignIpv6AddressOnCreation = true; publicSubnet.addDependency(ipv6CidrBlock); } const publicSubnetRtAssoc = new ec2.CfnSubnetRouteTableAssociation( this, `PublicSubnet${azUpper}RouteTableAssociation`, { subnetId: publicSubnet.ref, routeTableId: publicRouteTable.ref, } ); ((publicSubnetRtAssoc.node.defaultChild as cdk.CfnResource) ?? publicSubnetRtAssoc).overrideLogicalId( `PublicSubnet${azUpper}RouteTableAssociation` ); // Store reference this.publicSubnets.set( azLetter, ec2.Subnet.fromSubnetAttributes(this, `PublicSubnet${azUpper}Import`, { subnetId: publicSubnet.ref, availabilityZone: azName, routeTableId: publicRouteTable.ref, }) ); if (createPrivateSubnets) { // NAT Gateway EIP const natEip = new ec2.CfnEIP(this, `NATZone${azUpper}EIP`, { domain: 'vpc', }); natEip.addDependency(igw); ((natEip.node.defaultChild as cdk.CfnResource) ?? natEip).overrideLogicalId(`NATZone${azUpper}EIP`); this.natEips.set(azLetter, natEip); // NAT Gateway const natGateway = new ec2.CfnNatGateway(this, `NATGatewayZone${azUpper}`, { allocationId: natEip.attrAllocationId, subnetId: publicSubnet.ref, }); natGateway.addDependency(igw); ((natGateway.node.defaultChild as cdk.CfnResource) ?? natGateway).overrideLogicalId(`NATGatewayZone${azUpper}`); // Private Subnet 1 Route Table const privateRouteTable1 = new ec2.CfnRouteTable(this, `PrivateSubnet${azUpper}1RouteTable`, { vpcId: cfnVpc.ref, tags: [ { key: 'Name', value: `Private Subnet ${azUpper}1` }, { key: 'Network', value: 'Private' }, ], }); ((privateRouteTable1.node.defaultChild as cdk.CfnResource) ?? privateRouteTable1).overrideLogicalId( `PrivateSubnet${azUpper}1RouteTable` ); privateRouteTables.push(privateRouteTable1); privateSubnet1RouteTables.push(privateRouteTable1); const privateRoute1 = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}1Route`, { routeTableId: privateRouteTable1.ref, destinationCidrBlock: '0.0.0.0/0', natGatewayId: natGateway.ref, }); ((privateRoute1.node.defaultChild as cdk.CfnResource) ?? privateRoute1).overrideLogicalId( `PrivateSubnet${azUpper}1Route` ); // Private Subnet 1 const privateSubnet1 = new ec2.CfnSubnet(this, `PrivateSubnet${azUpper}1`, { vpcId: cfnVpc.ref, cidrBlock: cidrs.private1Cidr, availabilityZone: azName, tags: [ { key: 'Name', value: `Private Subnet ${azUpper}1` }, ...(privateSubnetATagParsed ? [ { key: privateSubnetATagParsed.key, value: privateSubnetATagParsed.value, }, ] : []), ], }); ((privateSubnet1.node.defaultChild as cdk.CfnResource) ?? privateSubnet1).overrideLogicalId( `PrivateSubnet${azUpper}1` ); const privateSubnet1RtAssoc = new ec2.CfnSubnetRouteTableAssociation( this, `PrivateSubnet${azUpper}1RouteTableAssociation`, { subnetId: privateSubnet1.ref, routeTableId: privateRouteTable1.ref, } ); ((privateSubnet1RtAssoc.node.defaultChild as cdk.CfnResource) ?? privateSubnet1RtAssoc).overrideLogicalId( `PrivateSubnet${azUpper}1RouteTableAssociation` ); // Assign IPv6 /64 to private subnet 1 (offset by numberOfAzs to avoid public subnet range) if (enableIpv6 && ipv6CidrBlock && egressOnlyIgw) { const private1Ipv6Cidr = cdk.Fn.select( numberOfAzs + index, cdk.Fn.cidr(cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks), 256, '64') ); privateSubnet1.ipv6CidrBlock = private1Ipv6Cidr; privateSubnet1.assignIpv6AddressOnCreation = true; privateSubnet1.addDependency(ipv6CidrBlock); // IPv6 egress route via Egress-Only IGW const private1Ipv6Route = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}1Ipv6Route`, { routeTableId: privateRouteTable1.ref, destinationIpv6CidrBlock: '::/0', egressOnlyInternetGatewayId: egressOnlyIgw.ref, }); ((private1Ipv6Route.node.defaultChild as cdk.CfnResource) ?? private1Ipv6Route).overrideLogicalId( `PrivateSubnet${azUpper}1Ipv6Route` ); } this.privateSubnets1.set( azLetter, ec2.Subnet.fromSubnetAttributes(this, `PrivateSubnet${azUpper}1Import`, { subnetId: privateSubnet1.ref, availabilityZone: azName, routeTableId: privateRouteTable1.ref, }) ); // Private Subnet 2 (with NACL) - only if enabled if (createAdditionalPrivateSubnets && cidrs.private2Cidr) { // Private Subnet 2 Route Table const privateRouteTable2 = new ec2.CfnRouteTable(this, `PrivateSubnet${azUpper}2RouteTable`, { vpcId: cfnVpc.ref, tags: [ { key: 'Name', value: `Private Subnet ${azUpper}2` }, { key: 'Network', value: 'Private' }, ], }); ((privateRouteTable2.node.defaultChild as cdk.CfnResource) ?? privateRouteTable2).overrideLogicalId( `PrivateSubnet${azUpper}2RouteTable` ); privateRouteTables.push(privateRouteTable2); const privateRoute2 = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}2Route`, { routeTableId: privateRouteTable2.ref, destinationCidrBlock: '0.0.0.0/0', natGatewayId: natGateway.ref, }); ((privateRoute2.node.defaultChild as cdk.CfnResource) ?? privateRoute2).overrideLogicalId( `PrivateSubnet${azUpper}2Route` ); // Private Subnet 2 const privateSubnet2 = new ec2.CfnSubnet(this, `PrivateSubnet${azUpper}2`, { vpcId: cfnVpc.ref, cidrBlock: cidrs.private2Cidr, availabilityZone: azName, tags: [ { key: 'Name', value: `Private Subnet ${azUpper}2` }, ...(privateSubnetBTagParsed ? [ { key: privateSubnetBTagParsed.key, value: privateSubnetBTagParsed.value, }, ] : []), ], }); ((privateSubnet2.node.defaultChild as cdk.CfnResource) ?? privateSubnet2).overrideLogicalId( `PrivateSubnet${azUpper}2` ); const privateSubnet2RtAssoc = new ec2.CfnSubnetRouteTableAssociation( this, `PrivateSubnet${azUpper}2RouteTableAssociation`, { subnetId: privateSubnet2.ref, routeTableId: privateRouteTable2.ref, } ); ((privateSubnet2RtAssoc.node.defaultChild as cdk.CfnResource) ?? privateSubnet2RtAssoc).overrideLogicalId( `PrivateSubnet${azUpper}2RouteTableAssociation` ); // Assign IPv6 /64 to private subnet 2 (offset by 2*numberOfAzs) if (enableIpv6 && ipv6CidrBlock && egressOnlyIgw) { const private2Ipv6Cidr = cdk.Fn.select( 2 * numberOfAzs + index, cdk.Fn.cidr(cdk.Fn.select(0, cfnVpc.attrIpv6CidrBlocks), 256, '64') ); privateSubnet2.ipv6CidrBlock = private2Ipv6Cidr; privateSubnet2.assignIpv6AddressOnCreation = true; privateSubnet2.addDependency(ipv6CidrBlock); // IPv6 egress route via Egress-Only IGW const private2Ipv6Route = new ec2.CfnRoute(this, `PrivateSubnet${azUpper}2Ipv6Route`, { routeTableId: privateRouteTable2.ref, destinationIpv6CidrBlock: '::/0', egressOnlyInternetGatewayId: egressOnlyIgw.ref, }); ((private2Ipv6Route.node.defaultChild as cdk.CfnResource) ?? private2Ipv6Route).overrideLogicalId( `PrivateSubnet${azUpper}2Ipv6Route` ); } // Network ACL for Private Subnet 2 const nacl = new ec2.CfnNetworkAcl(this, `PrivateSubnet${azUpper}2NetworkACL`, { vpcId: cfnVpc.ref, tags: [ { key: 'Name', value: `NACL Protected subnet ${azUpper}` }, { key: 'Network', value: 'NACL Protected' }, ], }); ((nacl.node.defaultChild as cdk.CfnResource) ?? nacl).overrideLogicalId(`PrivateSubnet${azUpper}2NetworkACL`); // Allow all inbound const naclInbound = new ec2.CfnNetworkAclEntry(this, `PrivateSubnet${azUpper}2NetworkACLEntryInbound`, { networkAclId: nacl.ref, ruleNumber: 100, protocol: -1, ruleAction: 'allow', egress: false, cidrBlock: '0.0.0.0/0', }); ((naclInbound.node.defaultChild as cdk.CfnResource) ?? naclInbound).overrideLogicalId( `PrivateSubnet${azUpper}2NetworkACLEntryInbound` ); // Allow all outbound const naclOutbound = new ec2.CfnNetworkAclEntry(this, `PrivateSubnet${azUpper}2NetworkACLEntryOutbound`, { networkAclId: nacl.ref, ruleNumber: 100, protocol: -1, ruleAction: 'allow', egress: true, cidrBlock: '0.0.0.0/0', }); ((naclOutbound.node.defaultChild as cdk.CfnResource) ?? naclOutbound).overrideLogicalId( `PrivateSubnet${azUpper}2NetworkACLEntryOutbound` ); // IPv6 NACL entries if (enableIpv6) { const naclInboundIpv6 = new ec2.CfnNetworkAclEntry( this, `PrivateSubnet${azUpper}2NetworkACLEntryInboundIpv6`, { networkAclId: nacl.ref, ruleNumber: 101, protocol: -1, ruleAction: 'allow', egress: false, ipv6CidrBlock: '::/0', } ); ((naclInboundIpv6.node.defaultChild as cdk.CfnResource) ?? naclInboundIpv6).overrideLogicalId( `PrivateSubnet${azUpper}2NetworkACLEntryInboundIpv6` ); const naclOutboundIpv6 = new ec2.CfnNetworkAclEntry( this, `PrivateSubnet${azUpper}2NetworkACLEntryOutboundIpv6`, { networkAclId: nacl.ref, ruleNumber: 101, protocol: -1, ruleAction: 'allow', egress: true, ipv6CidrBlock: '::/0', } ); ((naclOutboundIpv6.node.defaultChild as cdk.CfnResource) ?? naclOutboundIpv6).overrideLogicalId( `PrivateSubnet${azUpper}2NetworkACLEntryOutboundIpv6` ); } const naclAssociation = new ec2.CfnSubnetNetworkAclAssociation( this, `PrivateSubnet${azUpper}2NetworkACLAssociation`, { subnetId: privateSubnet2.ref, networkAclId: nacl.ref, } ); ((naclAssociation.node.defaultChild as cdk.CfnResource) ?? naclAssociation).overrideLogicalId( `PrivateSubnet${azUpper}2NetworkACLAssociation` ); this.privateSubnets2.set( azLetter, ec2.Subnet.fromSubnetAttributes(this, `PrivateSubnet${azUpper}2Import`, { subnetId: privateSubnet2.ref, availabilityZone: azName, routeTableId: privateRouteTable2.ref, }) ); } } }); // Create VPC from the L1 construct for use with L2 constructs // Build route table IDs arrays matching the subnet order const publicSubnetRouteTableIds = Array.from(this.publicSubnets.values()).map(() => publicRouteTable.ref); const privateSubnetRouteTableIds = privateSubnet1RouteTables.map((rt) => rt.ref); this.vpc = ec2.Vpc.fromVpcAttributes(this, 'VpcImport', { vpcId: cfnVpc.ref, availabilityZones: availabilityZones, publicSubnetIds: Array.from(this.publicSubnets.values()).map((s) => s.subnetId), privateSubnetIds: Array.from(this.privateSubnets1.values()).map((s) => s.subnetId), publicSubnetRouteTableIds: publicSubnetRouteTableIds, privateSubnetRouteTableIds: privateSubnetRouteTableIds, }); // S3 VPC Endpoint (Gateway type - free) if (createPrivateSubnets && createVpcEndpoints) { const s3Endpoint = new ec2.CfnVPCEndpoint(this, 'S3VPCEndpoint', { vpcId: cfnVpc.ref, serviceName: `com.amazonaws.${cdk.Stack.of(this).region}.s3`, routeTableIds: privateRouteTables.map((rt) => rt.ref), policyDocument: { Version: '2012-10-17', Statement: [ { Action: '*', Effect: 'Allow', Resource: '*', Principal: '*', }, ], }, }); ((s3Endpoint.node.defaultChild as cdk.CfnResource) ?? s3Endpoint).overrideLogicalId('S3VPCEndpoint'); // DynamoDB VPC Endpoint (Gateway type - free) const dynamoDbEndpoint = new ec2.CfnVPCEndpoint(this, 'DynamoDBVPCEndpoint', { vpcId: cfnVpc.ref, serviceName: `com.amazonaws.${cdk.Stack.of(this).region}.dynamodb`, routeTableIds: privateRouteTables.map((rt) => rt.ref), policyDocument: { Version: '2012-10-17', Statement: [ { Action: '*', Effect: 'Allow', Resource: '*', Principal: '*', }, ], }, }); ((dynamoDbEndpoint.node.defaultChild as cdk.CfnResource) ?? dynamoDbEndpoint).overrideLogicalId( 'DynamoDBVPCEndpoint' ); // Interface VPC Endpoints (cost ~$0.01/hr each, but eliminate NAT data processing) const endpointConfig = props.interfaceEndpoints ?? {}; // Security group for all interface endpoints const endpointSecurityGroup = new ec2.CfnSecurityGroup(this, 'VpcEndpointSecurityGroup', { vpcId: cfnVpc.ref, groupDescription: 'Security group for VPC Interface Endpoints', securityGroupIngress: [ { ipProtocol: 'tcp', fromPort: 443, toPort: 443, cidrIp: vpcCidr, description: 'Allow HTTPS from VPC IPv4', }, ...(enableIpv6 ? [ { ipProtocol: 'tcp', fromPort: 443, toPort: 443, cidrIpv6: '::/0', description: 'Allow HTTPS from VPC IPv6', }, ] : []), ], }); ((endpointSecurityGroup.node.defaultChild as cdk.CfnResource) ?? endpointSecurityGroup).overrideLogicalId( 'VpcEndpointSecurityGroup' ); const privateSubnetIds = Array.from(this.privateSubnets1.values()).map((s) => s.subnetId); const region = cdk.Stack.of(this).region; const interfaceEndpointServices: { id: string; service: string; enabled: boolean }[] = [ { id: 'ECRApiEndpoint', service: `com.amazonaws.${region}.ecr.api`, enabled: endpointConfig.ecr ?? true }, { id: 'ECRDockerEndpoint', service: `com.amazonaws.${region}.ecr.dkr`, enabled: endpointConfig.ecrDocker ?? true, }, { id: 'CloudWatchLogsEndpoint', service: `com.amazonaws.${region}.logs`, enabled: endpointConfig.cloudwatchLogs ?? true, }, { id: 'STSEndpoint', service: `com.amazonaws.${region}.sts`, enabled: endpointConfig.sts ?? true }, { id: 'SecretsManagerEndpoint', service: `com.amazonaws.${region}.secretsmanager`, enabled: endpointConfig.secretsManager ?? true, }, { id: 'SSMEndpoint', service: `com.amazonaws.${region}.ssm`, enabled: endpointConfig.ssm ?? true }, ]; for (const ep of interfaceEndpointServices) { if (!ep.enabled) continue; const endpoint = new ec2.CfnVPCEndpoint(this, ep.id, { vpcId: cfnVpc.ref, serviceName: ep.service, vpcEndpointType: 'Interface', privateDnsEnabled: true, subnetIds: privateSubnetIds, securityGroupIds: [endpointSecurityGroup.ref], }); ((endpoint.node.defaultChild as cdk.CfnResource) ?? endpoint).overrideLogicalId(ep.id); } } // Add CloudFormation Outputs matching the original template this.addOutputs( cfnVpc, azLetters, publicRouteTable, privateRouteTables, createPrivateSubnets, createAdditionalPrivateSubnets, enableIpv6 ); } /** * Parse a tag string in "Key=Value" format */ private parseTag(tagString: string): { key: string; value: string } | null { if (!tagString || !tagString.includes('=')) { return null; } const [key, ...valueParts] = tagString.split('='); return { key, value: valueParts.join('=') }; } /** * Add CloudFormation outputs for cross-stack references * Output logical IDs match the original CloudFormation template */ private addOutputs( cfnVpc: ec2.CfnVPC, azLetters: string[], publicRouteTable: ec2.CfnRouteTable, privateRouteTables: ec2.CfnRouteTable[], createPrivateSubnets: boolean, createAdditionalPrivateSubnets: boolean, enableIpv6: boolean ): void { const stack = cdk.Stack.of(this); // VPC outputs const vpcIdOutput = new cdk.CfnOutput(this, 'VPCID', { value: cfnVpc.ref, description: 'VPC ID', exportName: `${stack.stackName}-VPCID`, }); vpcIdOutput.overrideLogicalId('VPCID'); const vpcCidrOutput = new cdk.CfnOutput(this, 'VPCCIDR', { value: cfnVpc.cidrBlock!, description: 'VPC CIDR', exportName: `${stack.stackName}-VPCCIDR`, }); vpcCidrOutput.overrideLogicalId('VPCCIDR'); // Number of AZs output (for importing by other stacks) const numberOfAzsOutput = new cdk.CfnOutput(this, 'NumberOfAZs', { value: azLetters.length.toString(), description: 'Number of Availability Zones', exportName: `${stack.stackName}-NumberOfAZs`, }); numberOfAzsOutput.overrideLogicalId('NumberOfAZs'); // Public Route Table output const publicRtOutput = new cdk.CfnOutput(this, 'PublicSubnetRouteTableOutput', { value: publicRouteTable.ref, description: 'Public Subnet Route Table', exportName: `${stack.stackName}-PublicSubnetRouteTable`, }); publicRtOutput.overrideLogicalId('PublicSubnetRouteTableOutput'); // Public subnet outputs azLetters.forEach((azLetter) => { const azUpper = azLetter.toUpperCase(); const subnet = this.publicSubnets.get(azLetter); if (subnet) { const output = new cdk.CfnOutput(this, `PublicSubnet${azUpper}ID`, { value: subnet.subnetId, description: `Public Subnet ${azUpper} ID`, exportName: `${stack.stackName}-PublicSubnet${azUpper}ID`, }); output.overrideLogicalId(`PublicSubnet${azUpper}ID`); } }); // Private subnet outputs if (createPrivateSubnets) { azLetters.forEach((azLetter, index) => { const azUpper = azLetter.toUpperCase(); // Private Subnet 1 const subnet1 = this.privateSubnets1.get(azLetter); if (subnet1) { const output = new cdk.CfnOutput(this, `PrivateSubnet${azUpper}1ID`, { value: subnet1.subnetId, description: `Private Subnet 1 ID in Availability Zone ${azUpper}`, exportName: `${stack.stackName}-PrivateSubnet${azUpper}1ID`, }); output.overrideLogicalId(`PrivateSubnet${azUpper}1ID`); } // NAT EIP Output const natEip = this.natEips.get(azLetter); if (natEip) { const output = new cdk.CfnOutput(this, `NATZone${azUpper}EIPOutput`, { value: natEip.ref, description: `NAT ${azUpper} IP address`, exportName: `${stack.stackName}-NATZone${azUpper}EIP`, }); output.overrideLogicalId(`NATZone${azUpper}EIPOutput`); } // Private Route Table 1 Output const rtIndex = index * (createAdditionalPrivateSubnets ? 2 : 1); if (privateRouteTables[rtIndex]) { const output = new cdk.CfnOutput(this, `PrivateSubnet${azUpper}1RouteTableOutput`, { value: privateRouteTables[rtIndex].ref, description: `Private Subnet ${azUpper}1 Route Table`, exportName: `${stack.stackName}-PrivateSubnet${azUpper}1RouteTable`, }); output.overrideLogicalId(`PrivateSubnet${azUpper}1RouteTableOutput`); } // Private Subnet 2 if (createAdditionalPrivateSubnets) { const subnet2 = this.privateSubnets2.get(azLetter); if (subnet2) { const output = new cdk.CfnOutput(this, `PrivateSubnet${azUpper}2ID`, { value: subnet2.subnetId, description: `Private Subnet 2 ID in Availability Zone ${azUpper}`, exportName: `${stack.stackName}-PrivateSubnet${azUpper}2ID`, }); output.overrideLogicalId(`PrivateSubnet${azUpper}2ID`); } // Private Route Table 2 Output if (privateRouteTables[rtIndex + 1]) { const output = new cdk.CfnOutput(this, `PrivateSubnet${azUpper}2RouteTableOutput`, { value: privateRouteTables[rtIndex + 1].ref, description: `Private Subnet ${azUpper}2 Route Table`, exportName: `${stack.stackName}-PrivateSubnet${azUpper}2RouteTable`, }); output.overrideLogicalId(`PrivateSubnet${azUpper}2RouteTableOutput`); } } }); } // IPv6 outputs if (enableIpv6 && this.egressOnlyInternetGateway) { const eoigwOutput = new cdk.CfnOutput(this, 'EgressOnlyInternetGatewayId', { value: this.egressOnlyInternetGateway.ref, description: 'Egress-Only Internet Gateway ID', exportName: `${stack.stackName}-EgressOnlyIGW`, }); eoigwOutput.overrideLogicalId('EgressOnlyInternetGatewayId'); } } }