Skip to content

Commit 397fb3e

Browse files
authored
fix: natgateway routing (#23)
* fix: natgateway routing * feat: added @smallcase/core-infra as approver
1 parent ff96b00 commit 397fb3e

File tree

4 files changed

+342
-29
lines changed

4 files changed

+342
-29
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
* @mr-robot-in
2-
* @hritik-verma-sc
3-
* @rithikb24
4-
* @gagan1510
1+
* @smallcase/core-infra

README.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,219 @@ Here’s a breakdown of the configuration options available:
293293
4. externalSubnets: Specify external subnets if you need to define subnets manually (each with an id, availabilityZone, and routeTableId).
294294
5. iamPolicyStatements: (Optional) Attach IAM policy statements to control access to the endpoint.
295295
6. additionalTags: (Optional) Add custom tags to the VPC Endpoint for easier identification and tracking.
296+
297+
## Dynamic Routing Strategy
298+
299+
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:
300+
301+
### **Single NAT Gateway (≤1 NAT Gateway)**
302+
- **Strategy**: One route table per subnet group
303+
- **Benefits**:
304+
- **Cost Optimized**: Fewer route tables = lower costs
305+
- **Simpler Management**: One route table per subnet group
306+
- **No Duplicate Routes**: Single NAT Gateway means no duplicate `0.0.0.0/0` entries
307+
- **Suitable for**: Development, testing, small workloads
308+
- **Configuration**: All subnets in a group share the same route table with one NAT route
309+
310+
### **Multiple NAT Gateways (>1 NAT Gateway)**
311+
- **Strategy**: One route table per subnet
312+
- **Benefits**:
313+
- **Prevents Duplicate Routes**: Each subnet gets its own route table, avoiding duplicate `0.0.0.0/0` entries
314+
- **AZ-Aware Routing**: Each subnet uses NAT Gateway in the same AZ when possible
315+
- **High Availability**: Better fault tolerance and load distribution
316+
- **Cross-AZ Cost Reduction**: Reduced data transfer costs
317+
- **Suitable for**: Production workloads, high availability requirements
318+
- **Configuration**: Each subnet gets its own route table with one NAT route
319+
320+
### **Why This Strategy?**
321+
322+
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:
323+
324+
- **❌ Wrong Approach**: Multiple NAT routes in same route table → `AlreadyExists` error
325+
- **✅ Correct Approach**: One route table per subnet → Each gets one NAT route
326+
327+
### **Automatic Strategy Selection**
328+
329+
```typescript
330+
// Single NAT Gateway - Cost Optimized
331+
new Network(this, 'NETWORK', {
332+
vpc: {
333+
cidr: '10.10.0.0/16',
334+
subnetConfiguration: [],
335+
},
336+
subnets: [
337+
{
338+
subnetGroupName: 'NATGateway',
339+
subnetType: ec2.SubnetType.PUBLIC,
340+
cidrBlock: ['10.10.0.0/28'], // Only one subnet
341+
availabilityZones: ['ap-south-1a'], // Only one AZ
342+
useSubnetForNAT: true,
343+
},
344+
{
345+
subnetGroupName: 'Private',
346+
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
347+
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
348+
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
349+
},
350+
],
351+
});
352+
353+
// Multiple NAT Gateways - Performance Optimized
354+
new Network(this, 'NETWORK', {
355+
vpc: {
356+
cidr: '10.10.0.0/16',
357+
subnetConfiguration: [],
358+
},
359+
natEipAllocationIds: [
360+
'eipalloc-1234567890abcdef',
361+
'eipalloc-0987654321fedcba',
362+
'eipalloc-abcdef1234567890',
363+
],
364+
subnets: [
365+
{
366+
subnetGroupName: 'NATGateway',
367+
subnetType: ec2.SubnetType.PUBLIC,
368+
cidrBlock: ['10.10.0.0/28', '10.10.0.16/28', '10.10.0.32/28'],
369+
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
370+
useSubnetForNAT: true,
371+
},
372+
{
373+
subnetGroupName: 'Private',
374+
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
375+
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
376+
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
377+
},
378+
],
379+
});
380+
```
381+
382+
### **CloudFormation Outputs**
383+
384+
The module provides outputs to show which strategy is being used:
385+
386+
- **RoutingStrategy**: Shows "Route Table per Subnet Group" or "Route Table per Subnet"
387+
- **NATGatewayCount**: Shows the number of NAT Gateways configured
388+
389+
### **Route Table Distribution**
390+
391+
| Scenario | Route Tables | NAT Routes per Table | Strategy |
392+
|----------|-------------|---------------------|----------|
393+
| Single NAT | 1 per subnet group | 1 | Cost optimized |
394+
| Multiple NAT | 1 per subnet | 1 | Performance optimized |
395+
396+
### **Migration Strategy**
397+
398+
You can easily migrate between strategies by changing your configuration:
399+
400+
1. **Development → Production**: Add more NAT Gateway subnets
401+
2. **Production → Development**: Reduce to single NAT Gateway subnet
402+
3. **Cost Optimization**: Monitor usage and adjust NAT Gateway count
403+
404+
The module handles the migration automatically without manual route table changes.
405+
406+
## NAT Gateway EIP Allocation
407+
408+
You can specify existing Elastic IP (EIP) allocation IDs to use with your NAT Gateways. This is useful when you want to:
409+
410+
- Use pre-existing EIPs
411+
- Maintain consistent IP addresses across deployments
412+
- Control EIP costs and management
413+
414+
### **Using EIP Allocation IDs**
415+
416+
```typescript
417+
new Network(this, 'NETWORK', {
418+
vpc: {
419+
cidr: '10.10.0.0/16',
420+
subnetConfiguration: [],
421+
},
422+
// Specify existing EIP allocation IDs
423+
natEipAllocationIds: [
424+
'eipalloc-1234567890abcdef', // EIP for NAT Gateway 1
425+
'eipalloc-0987654321fedcba', // EIP for NAT Gateway 2
426+
'eipalloc-abcdef1234567890', // EIP for NAT Gateway 3
427+
],
428+
subnets: [
429+
{
430+
subnetGroupName: 'NATGateway',
431+
subnetType: ec2.SubnetType.PUBLIC,
432+
cidrBlock: ['10.10.0.0/28', '10.10.0.16/28', '10.10.0.32/28'],
433+
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
434+
useSubnetForNAT: true,
435+
},
436+
{
437+
subnetGroupName: 'Private',
438+
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
439+
cidrBlock: ['10.10.5.0/24', '10.10.6.0/24', '10.10.7.0/24'],
440+
availabilityZones: ['ap-south-1a', 'ap-south-1b', 'ap-south-1c'],
441+
},
442+
],
443+
});
444+
```
445+
446+
### **EIP Allocation ID Requirements**
447+
448+
- **Count**: Should match the number of NAT Gateway subnets (optional)
449+
- **Format**: Must be valid EIP allocation IDs (e.g., `eipalloc-xxxxxxxxx`)
450+
- **Region**: Must be in the same region as your VPC
451+
- **Account**: Must be owned by the same AWS account
452+
453+
### **Benefits of Using EIP Allocation IDs**
454+
455+
1. **Cost Control**: Reuse existing EIPs instead of creating new ones
456+
2. **IP Consistency**: Maintain the same public IP addresses across deployments
457+
3. **Compliance**: Meet requirements for static IP addresses
458+
4. **DNS**: Use existing DNS records that point to specific EIPs
459+
460+
### **CloudFormation Outputs**
461+
462+
When EIP allocation IDs are provided, the module outputs:
463+
464+
- **NATEipAllocationIds**: Comma-separated list of EIP allocation IDs used
465+
- **RoutingStrategy**: Shows the routing strategy being used
466+
- **NATGatewayCount**: Number of NAT Gateways configured
467+
468+
### **Example Output**
469+
470+
```json
471+
{
472+
"NATEipAllocationIds": "eipalloc-1234567890abcdef,eipalloc-0987654321fedcba,eipalloc-abcdef1234567890",
473+
"RoutingStrategy": "Route Table per Subnet",
474+
"NATGatewayCount": "3"
475+
}
476+
```
477+
478+
### **Creating EIPs for NAT Gateways**
479+
480+
If you need to create EIPs first, you can do so using CDK:
481+
482+
```typescript
483+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
484+
485+
// Create EIPs
486+
const eip1 = new ec2.CfnEIP(this, 'NATEip1', {
487+
domain: 'vpc',
488+
});
489+
490+
const eip2 = new ec2.CfnEIP(this, 'NATEip2', {
491+
domain: 'vpc',
492+
});
493+
494+
const eip3 = new ec2.CfnEIP(this, 'NATEip3', {
495+
domain: 'vpc',
496+
});
497+
498+
// Use them in your Network construct
499+
new Network(this, 'NETWORK', {
500+
vpc: {
501+
cidr: '10.10.0.0/16',
502+
subnetConfiguration: [],
503+
},
504+
natEipAllocationIds: [
505+
eip1.attrAllocationId,
506+
eip2.attrAllocationId,
507+
eip3.attrAllocationId,
508+
],
509+
// ... rest of configuration
510+
});
511+
```

src/constructs/network.ts

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ export class Network extends Construct {
186186
vpcId: this.vpc.vpcId,
187187
});
188188

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

229-
// Second pass: configure routes after NAT is configured
230-
props.subnets.forEach((subnetProps) => {
231-
const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}RouteTableManager`, {
232-
vpc: this.vpc,
233-
subnetGroupName: subnetProps.subnetGroupName,
234-
routes: subnetProps.routes,
235-
peeringConnectionId: this.peeringConnectionIds,
236-
subnetType: subnetProps.subnetType,
237-
natProvider: natProvider,
238-
internetGateway: internetGateway,
239-
});
240-
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
241-
routeTableManager.associateSubnet(subnet, index);
242-
});
229+
// Determine routing strategy based on number of NAT Gateways
230+
const natGatewayCount = this.natSubnets.length;
231+
const useSingleRouteTable = natGatewayCount <= 1;
232+
233+
// Add output to show which strategy is being used
234+
new CfnOutput(this, 'RoutingStrategy', {
235+
value: useSingleRouteTable ? 'Route Table per Subnet Group' : 'Route Table per Subnet',
236+
description: 'Routing strategy based on NAT Gateway count',
237+
});
238+
239+
new CfnOutput(this, 'NATGatewayCount', {
240+
value: natGatewayCount.toString(),
241+
description: 'Number of NAT Gateways configured',
243242
});
244243

244+
// Add output for EIP allocation IDs if provided
245+
if (props.natEipAllocationIds && props.natEipAllocationIds.length > 0) {
246+
new CfnOutput(this, 'NATEipAllocationIds', {
247+
value: props.natEipAllocationIds.join(','),
248+
description: 'EIP allocation IDs used for NAT Gateways',
249+
});
250+
}
251+
252+
if (useSingleRouteTable) {
253+
// Single NAT Gateway: One route table per subnet group
254+
this.configureSubnetGroupRouteTables(props, natProvider, internetGateway);
255+
} else {
256+
// Multiple NAT Gateways: One route table per subnet to avoid duplicate 0.0.0.0/0 entries
257+
this.configureSubnetRouteTables(props, natProvider, internetGateway);
258+
}
259+
245260
// this.pbSubnets.forEach((pb) => {
246261
// pb.addDefaultInternetRoute(internetGateway.ref, att);
247262
// });
@@ -260,6 +275,76 @@ export class Network extends Construct {
260275
}
261276
}
262277

278+
/**
279+
* Configure route tables per subnet group (for single NAT Gateway)
280+
*/
281+
private configureSubnetGroupRouteTables(
282+
props: VPCProps,
283+
natProvider: ec2.NatProvider,
284+
internetGateway: ec2.CfnInternetGateway,
285+
) {
286+
// One route table per subnet group
287+
props.subnets.forEach((subnetProps) => {
288+
const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}RouteTableManager`, {
289+
vpc: this.vpc,
290+
subnetGroupName: subnetProps.subnetGroupName,
291+
routes: subnetProps.routes,
292+
peeringConnectionId: this.peeringConnectionIds,
293+
subnetType: subnetProps.subnetType,
294+
natProvider: natProvider,
295+
internetGateway: internetGateway,
296+
});
297+
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
298+
routeTableManager.associateSubnet(subnet, index);
299+
});
300+
});
301+
}
302+
303+
/**
304+
* Configure route tables per subnet (for multiple NAT Gateways)
305+
* This prevents duplicate 0.0.0/0 entries in the same route table
306+
*/
307+
private configureSubnetRouteTables(
308+
props: VPCProps,
309+
natProvider: ec2.NatProvider,
310+
internetGateway: ec2.CfnInternetGateway,
311+
) {
312+
// One route table per subnet to avoid duplicate 0.0.0.0/0 entries
313+
props.subnets.forEach((subnetProps) => {
314+
this.subnets[subnetProps.subnetGroupName].forEach((subnet, index) => {
315+
// Find the NAT Gateway in the same AZ as the subnet
316+
//let specificNatGateway: ec2.CfnNatGateway | undefined;
317+
// if (subnetProps.subnetType === ec2.SubnetType.PRIVATE_WITH_NAT && natProvider.configuredGateways) {
318+
// // Try to find NAT Gateway in the same AZ
319+
// const subnetAZ = subnet.availabilityZone;
320+
// const natGatewayInSameAZ = natProvider.configuredGateways.find(natGateway => {
321+
// return natGateway.az === subnetAZ;
322+
// });
323+
324+
// if (natGatewayInSameAZ) {
325+
// // Use the NAT Gateway from the same AZ
326+
// specificNatGateway = natGatewayInSameAZ as any; // Type assertion for CfnNatGateway
327+
// } else {
328+
// // Fallback to first NAT Gateway if no match found
329+
// specificNatGateway = natProvider.configuredGateways[0] as any;
330+
// }
331+
// }
332+
333+
const routeTableManager = new RouteTableManager(this, `${subnetProps.subnetGroupName}Subnet${index}RouteTableManager`, {
334+
vpc: this.vpc,
335+
subnetGroupName: `${subnetProps.subnetGroupName}Subnet${index}`,
336+
routes: subnetProps.routes,
337+
peeringConnectionId: this.peeringConnectionIds,
338+
subnetType: subnetProps.subnetType,
339+
natProvider: natProvider,
340+
internetGateway: internetGateway,
341+
subnetAvailabilityZone: subnet.availabilityZone,
342+
});
343+
routeTableManager.associateSubnet(subnet, 0); // Only one subnet per route table
344+
});
345+
});
346+
}
347+
263348
createSubnet(option: ISubnetsProps, vpc: ec2.Vpc) {
264349
const subnets: ec2.Subnet[] = [];
265350
const SUBNETTYPE_TAG = 'aws-cdk:subnet-type';

0 commit comments

Comments
 (0)