Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
* @mr-robot-in
* @hritik-verma-sc
* @rithikb24
* @gagan1510
* @smallcase/core-infra
216 changes: 216 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,219 @@ Here’s a breakdown of the configuration options available:
4. externalSubnets: Specify external subnets if you need to define subnets manually (each with an id, availabilityZone, and routeTableId).
5. iamPolicyStatements: (Optional) Attach IAM policy statements to control access to the endpoint.
6. additionalTags: (Optional) Add custom tags to the VPC Endpoint for easier identification and tracking.

## Dynamic Routing Strategy

The module automatically chooses the optimal routing strategy based on the number of NAT Gateways to prevent duplicate `0.0.0.0/0` route entries:

### **Single NAT Gateway (≤1 NAT Gateway)**
- **Strategy**: One route table per subnet group
- **Benefits**:
- **Cost Optimized**: Fewer route tables = lower costs
- **Simpler Management**: One route table per subnet group
- **No Duplicate Routes**: Single NAT Gateway means no duplicate `0.0.0.0/0` entries
- **Suitable for**: Development, testing, small workloads
- **Configuration**: All subnets in a group share the same route table with one NAT route

### **Multiple NAT Gateways (>1 NAT Gateway)**
- **Strategy**: One route table per subnet
- **Benefits**:
- **Prevents Duplicate Routes**: Each subnet gets its own route table, avoiding duplicate `0.0.0.0/0` entries
- **AZ-Aware Routing**: Each subnet uses NAT Gateway in the same AZ when possible
- **High Availability**: Better fault tolerance and load distribution
- **Cross-AZ Cost Reduction**: Reduced data transfer costs
- **Suitable for**: Production workloads, high availability requirements
- **Configuration**: Each subnet gets its own route table with one NAT route

### **Why This Strategy?**

AWS route tables don't support duplicate entries for the same destination CIDR (like `0.0.0.0/0`). When you have multiple NAT Gateways:

- **❌ Wrong Approach**: Multiple NAT routes in same route table → `AlreadyExists` error
- **✅ Correct Approach**: One route table per subnet → Each gets one NAT route

### **Automatic Strategy Selection**

```typescript
// Single NAT Gateway - Cost Optimized
new Network(this, 'NETWORK', {
vpc: {
cidr: '10.10.0.0/16',
subnetConfiguration: [],
},
subnets: [
{
subnetGroupName: 'NATGateway',
subnetType: ec2.SubnetType.PUBLIC,
cidrBlock: ['10.10.0.0/28'], // Only one subnet
availabilityZones: ['ap-south-1a'], // Only one AZ
useSubnetForNAT: true,
},
{
subnetGroupName: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
},
],
});

// Multiple NAT Gateways - Performance Optimized
new Network(this, 'NETWORK', {
vpc: {
cidr: '10.10.0.0/16',
subnetConfiguration: [],
},
natEipAllocationIds: [
'eipalloc-1234567890abcdef',
'eipalloc-0987654321fedcba',
'eipalloc-abcdef1234567890',
],
subnets: [
{
subnetGroupName: 'NATGateway',
subnetType: ec2.SubnetType.PUBLIC,
cidrBlock: ['10.10.0.0/28', '10.10.0.16/28', '10.10.0.32/28'],
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
useSubnetForNAT: true,
},
{
subnetGroupName: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
},
],
});
```

### **CloudFormation Outputs**

The module provides outputs to show which strategy is being used:

- **RoutingStrategy**: Shows "Route Table per Subnet Group" or "Route Table per Subnet"
- **NATGatewayCount**: Shows the number of NAT Gateways configured

### **Route Table Distribution**

| Scenario | Route Tables | NAT Routes per Table | Strategy |
|----------|-------------|---------------------|----------|
| Single NAT | 1 per subnet group | 1 | Cost optimized |
| Multiple NAT | 1 per subnet | 1 | Performance optimized |

### **Migration Strategy**

You can easily migrate between strategies by changing your configuration:

1. **Development → Production**: Add more NAT Gateway subnets
2. **Production → Development**: Reduce to single NAT Gateway subnet
3. **Cost Optimization**: Monitor usage and adjust NAT Gateway count

The module handles the migration automatically without manual route table changes.

## NAT Gateway EIP Allocation

You can specify existing Elastic IP (EIP) allocation IDs to use with your NAT Gateways. This is useful when you want to:

- Use pre-existing EIPs
- Maintain consistent IP addresses across deployments
- Control EIP costs and management

### **Using EIP Allocation IDs**

```typescript
new Network(this, 'NETWORK', {
vpc: {
cidr: '10.10.0.0/16',
subnetConfiguration: [],
},
// Specify existing EIP allocation IDs
natEipAllocationIds: [
'eipalloc-1234567890abcdef', // EIP for NAT Gateway 1
'eipalloc-0987654321fedcba', // EIP for NAT Gateway 2
'eipalloc-abcdef1234567890', // EIP for NAT Gateway 3
],
subnets: [
{
subnetGroupName: 'NATGateway',
subnetType: ec2.SubnetType.PUBLIC,
cidrBlock: ['10.10.0.0/28', '10.10.0.16/28', '10.10.0.32/28'],
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
useSubnetForNAT: true,
},
{
subnetGroupName: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
},
],
});
```

### **EIP Allocation ID Requirements**

- **Count**: Should match the number of NAT Gateway subnets (optional)
- **Format**: Must be valid EIP allocation IDs (e.g., `eipalloc-xxxxxxxxx`)
- **Region**: Must be in the same region as your VPC
- **Account**: Must be owned by the same AWS account

### **Benefits of Using EIP Allocation IDs**

1. **Cost Control**: Reuse existing EIPs instead of creating new ones
2. **IP Consistency**: Maintain the same public IP addresses across deployments
3. **Compliance**: Meet requirements for static IP addresses
4. **DNS**: Use existing DNS records that point to specific EIPs

### **CloudFormation Outputs**

When EIP allocation IDs are provided, the module outputs:

- **NATEipAllocationIds**: Comma-separated list of EIP allocation IDs used
- **RoutingStrategy**: Shows the routing strategy being used
- **NATGatewayCount**: Number of NAT Gateways configured

### **Example Output**

```json
{
"NATEipAllocationIds": "eipalloc-1234567890abcdef,eipalloc-0987654321fedcba,eipalloc-abcdef1234567890",
"RoutingStrategy": "Route Table per Subnet",
"NATGatewayCount": "3"
}
```

### **Creating EIPs for NAT Gateways**

If you need to create EIPs first, you can do so using CDK:

```typescript
import * as ec2 from 'aws-cdk-lib/aws-ec2';

// Create EIPs
const eip1 = new ec2.CfnEIP(this, 'NATEip1', {
domain: 'vpc',
});

const eip2 = new ec2.CfnEIP(this, 'NATEip2', {
domain: 'vpc',
});

const eip3 = new ec2.CfnEIP(this, 'NATEip3', {
domain: 'vpc',
});

// Use them in your Network construct
new Network(this, 'NETWORK', {
vpc: {
cidr: '10.10.0.0/16',
subnetConfiguration: [],
},
natEipAllocationIds: [
eip1.attrAllocationId,
eip2.attrAllocationId,
eip3.attrAllocationId,
],
// ... rest of configuration
});
```
117 changes: 101 additions & 16 deletions src/constructs/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export class Network extends Construct {
vpcId: this.vpc.vpcId,
});

// Initialize NAT provider after collecting all subnets
const natProvider = props.natEipAllocationIds?.length === this.natSubnets?.length && props.natEipAllocationIds?.length > 0
// Initialize NAT provider with EIP allocation IDs if provided
const natProvider = props.natEipAllocationIds && props.natEipAllocationIds.length > 0
? ec2.NatProvider.gateway({
eipAllocationIds: props.natEipAllocationIds,
}) : ec2.NatProvider.gateway();
Expand Down Expand Up @@ -226,22 +226,37 @@ export class Network extends Construct {
});
}

// Second pass: configure routes after NAT is configured
props.subnets.forEach((subnetProps) => {
const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}RouteTableManager`, {
vpc: this.vpc,
subnetGroupName: subnetProps.subnetGroupName,
routes: subnetProps.routes,
peeringConnectionId: this.peeringConnectionIds,
subnetType: subnetProps.subnetType,
natProvider: natProvider,
internetGateway: internetGateway,
});
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
routeTableManager.associateSubnet(subnet, index);
});
// Determine routing strategy based on number of NAT Gateways
const natGatewayCount = this.natSubnets.length;
const useSingleRouteTable = natGatewayCount <= 1;

// Add output to show which strategy is being used
new CfnOutput(this, 'RoutingStrategy', {
value: useSingleRouteTable ? 'Route Table per Subnet Group' : 'Route Table per Subnet',
description: 'Routing strategy based on NAT Gateway count',
});

new CfnOutput(this, 'NATGatewayCount', {
value: natGatewayCount.toString(),
description: 'Number of NAT Gateways configured',
});

// Add output for EIP allocation IDs if provided
if (props.natEipAllocationIds && props.natEipAllocationIds.length > 0) {
new CfnOutput(this, 'NATEipAllocationIds', {
value: props.natEipAllocationIds.join(','),
description: 'EIP allocation IDs used for NAT Gateways',
});
}

if (useSingleRouteTable) {
// Single NAT Gateway: One route table per subnet group
this.configureSubnetGroupRouteTables(props, natProvider, internetGateway);
} else {
// Multiple NAT Gateways: One route table per subnet to avoid duplicate 0.0.0.0/0 entries
this.configureSubnetRouteTables(props, natProvider, internetGateway);
}

// this.pbSubnets.forEach((pb) => {
// pb.addDefaultInternetRoute(internetGateway.ref, att);
// });
Expand All @@ -260,6 +275,76 @@ export class Network extends Construct {
}
}

/**
* Configure route tables per subnet group (for single NAT Gateway)
*/
private configureSubnetGroupRouteTables(
props: VPCProps,
natProvider: ec2.NatProvider,
internetGateway: ec2.CfnInternetGateway,
) {
// One route table per subnet group
props.subnets.forEach((subnetProps) => {
const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}RouteTableManager`, {
vpc: this.vpc,
subnetGroupName: subnetProps.subnetGroupName,
routes: subnetProps.routes,
peeringConnectionId: this.peeringConnectionIds,
subnetType: subnetProps.subnetType,
natProvider: natProvider,
internetGateway: internetGateway,
});
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
routeTableManager.associateSubnet(subnet, index);
});
});
}

/**
* Configure route tables per subnet (for multiple NAT Gateways)
* This prevents duplicate 0.0.0/0 entries in the same route table
*/
private configureSubnetRouteTables(
props: VPCProps,
natProvider: ec2.NatProvider,
internetGateway: ec2.CfnInternetGateway,
) {
// One route table per subnet to avoid duplicate 0.0.0.0/0 entries
props.subnets.forEach((subnetProps) => {
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
// Find the NAT Gateway in the same AZ as the subnet
//let specificNatGateway: ec2.CfnNatGateway | undefined;
// if (subnetProps.subnetType === ec2.SubnetType.PRIVATE_WITH_NAT && natProvider.configuredGateways) {
// // Try to find NAT Gateway in the same AZ
// const subnetAZ = subnet.availabilityZone;
// const natGatewayInSameAZ = natProvider.configuredGateways.find(natGateway => {
// return natGateway.az === subnetAZ;
// });

// if (natGatewayInSameAZ) {
// // Use the NAT Gateway from the same AZ
// specificNatGateway = natGatewayInSameAZ as any; // Type assertion for CfnNatGateway
// } else {
// // Fallback to first NAT Gateway if no match found
// specificNatGateway = natProvider.configuredGateways[0] as any;
// }
// }

const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}Subnet${index}RouteTableManager`, {
vpc: this.vpc,
subnetGroupName: `${subnetProps.subnetGroupName}Subnet${index}`,
routes: subnetProps.routes,
peeringConnectionId: this.peeringConnectionIds,
subnetType: subnetProps.subnetType,
natProvider: natProvider,
internetGateway: internetGateway,
subnetAvailabilityZone: subnet.availabilityZone,
});
routeTableManager.associateSubnet(subnet, 0); // Only one subnet per route table
});
});
}

createSubnet(option: ISubnetsProps, vpc: ec2.Vpc) {
const subnets: ec2.Subnet[] = [];
const SUBNETTYPE_TAG = 'aws-cdk:subnet-type';
Expand Down
Loading
Loading