Initial commit: Spicy CDK automation framework
Jenkins shared library and CDK constructs for AWS infrastructure. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
677
resources/lib/constructs/spicy-vpc.ts
Normal file
677
resources/lib/constructs/spicy-vpc.ts
Normal file
@@ -0,0 +1,677 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default CIDR configurations matching the original CloudFormation template
|
||||
*/
|
||||
const DEFAULT_SUBNET_CIDRS: Required<SpicyVpcProps['subnetCidrs']> = {
|
||||
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<string, ec2.ISubnet> = new Map();
|
||||
|
||||
/** Primary private subnets indexed by AZ letter */
|
||||
public readonly privateSubnets1: Map<string, ec2.ISubnet> = new Map();
|
||||
|
||||
/** Secondary private subnets (with NACLs) indexed by AZ letter */
|
||||
public readonly privateSubnets2: Map<string, ec2.ISubnet> = new Map();
|
||||
|
||||
/** NAT Gateway EIPs indexed by AZ letter */
|
||||
public readonly natEips: Map<string, ec2.CfnEIP> = new Map();
|
||||
|
||||
/** S3 Gateway Endpoint */
|
||||
public readonly s3Endpoint?: ec2.GatewayVpcEndpoint;
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
// 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');
|
||||
|
||||
// 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}`);
|
||||
|
||||
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`
|
||||
);
|
||||
|
||||
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`
|
||||
);
|
||||
|
||||
// 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`
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Add CloudFormation Outputs matching the original template
|
||||
this.addOutputs(
|
||||
cfnVpc,
|
||||
azLetters,
|
||||
publicRouteTable,
|
||||
privateRouteTables,
|
||||
createPrivateSubnets,
|
||||
createAdditionalPrivateSubnets
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
): 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`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user