Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
473ab77
feat(natgw): add CloudFormation template for Regional NAT Gateway setup
badmintoncryer Dec 31, 2025
6c3dd67
feat(ec2): enhance Regional NAT Gateway provider with allocationId an…
badmintoncryer Dec 31, 2025
e93aa99
Merge branch 'main' into regional-natgw
badmintoncryer Dec 31, 2025
a24a26c
feat(ec2): update NAT provider to support optional natSubnets for reg…
badmintoncryer Jan 1, 2026
0a044a3
Merge remote-tracking branch 'cryer/regional-natgw' into regional-natgw
badmintoncryer Jan 1, 2026
412860b
feat(natgw): add CloudFormation template for Regional NAT Gateway wit…
badmintoncryer Jan 1, 2026
25bf4fd
feat(ec2): add dependency for Regional NAT Gateway on VPC internet co…
badmintoncryer Jan 1, 2026
26b5d84
feat(ec2): add documentation and tests for Regional NAT Gateway funct…
badmintoncryer Jan 1, 2026
85dbfbb
Add integration test snapshots for regional NAT gateway setup
badmintoncryer Jan 1, 2026
f502f19
feat(ec2): improve error messages and simplify EIP allocation logic f…
badmintoncryer Jan 1, 2026
1431032
docs(ec2): update README for Regional NAT Gateway examples and remove…
badmintoncryer Jan 1, 2026
2f595bb
refactor(ec2): remove redundant comments in VPC NAT Gateway logic
badmintoncryer Jan 1, 2026
0ac01a3
fix(ec2): clarify dependency requirement for NAT Gateway creation
badmintoncryer Jan 1, 2026
8017f0d
test(ec2): add test for single NAT Gateway creation with Regional NAT…
badmintoncryer Jan 1, 2026
9d42d2a
fix(ec2): simplify NAT Gateway count logic and improve validation err…
badmintoncryer Jan 1, 2026
9007845
fix(ec2): update warning messages for Regional NAT Gateway to use bac…
badmintoncryer Jan 1, 2026
42522c7
fix(ec2): update Regional NAT Gateway logic to conditionally create E…
badmintoncryer Jan 1, 2026
0d1b9db
fix(ec2): remove redundant subnetConfiguration for regional NAT Gatew…
badmintoncryer Jan 1, 2026
9ca245e
docs(ec2): add link to Regional NAT Gateways documentation for clarity
badmintoncryer Jan 1, 2026
b720b2e
fix(ec2): streamline allocationId logic in Regional NAT Gateway provider
badmintoncryer Jan 1, 2026
f0d57fe
fix(ec2): update README to clarify usage of Regional NAT Gateway with…
badmintoncryer Jan 1, 2026
8d1c766
fix(ec2): update README to clarify Regional NAT Gateway's automatic E…
badmintoncryer Jan 1, 2026
2af3db8
fix(ec2): remove unused import of RegionalNatGatewayProvider from vpc…
badmintoncryer Jan 2, 2026
0a0956d
Merge remote-tracking branch 'origin/main' into regional-natgw
badmintoncryer Jan 2, 2026
306acaa
fix(ec2): add tests for NAT Gateway configurations and routing behavior
badmintoncryer Jan 2, 2026
773ffbb
fix(ec2): update ConfigureNatOptions documentation and add natGateway…
badmintoncryer Jan 2, 2026
c12feef
update integ
badmintoncryer Jan 4, 2026
86032d6
Merge branch 'main' into regional-natgw
badmintoncryer Jan 4, 2026
10f416d
Merge branch 'main' into regional-natgw
badmintoncryer Jan 5, 2026
b001201
Merge branch 'main' into regional-natgw
badmintoncryer Jan 12, 2026
243a35f
Merge branch 'main' into regional-natgw
badmintoncryer Jan 20, 2026
e239513
Update packages/aws-cdk-lib/aws-ec2/lib/vpc.ts
badmintoncryer Feb 4, 2026
30dfb7e
fix: update NAT configuration options to use ConfigureRegionalNatOptions
badmintoncryer Feb 7, 2026
46168d1
feat: add maxDrainDuration option to NAT Gateway configuration
badmintoncryer Feb 7, 2026
02bff45
fix: simplify private subnet filtering in NAT Gateway configuration
badmintoncryer Feb 8, 2026
43e535c
Merge remote-tracking branch 'origin/main' into regional-natgw
badmintoncryer Feb 8, 2026
ed2029d
fix: update NAT configuration to use ConfigureNatOptions and handle n…
badmintoncryer Feb 21, 2026
bf06cd9
Merge remote-tracking branch 'origin/main' into regional-natgw
badmintoncryer Feb 21, 2026
b1628a8
fix: update import statements for NAT and VPC modules to use type imp…
badmintoncryer Feb 23, 2026
184729c
Merge branch 'main' into regional-natgw
badmintoncryer Feb 23, 2026
368f92a
Merge remote-tracking branch 'origin/main' into regional-natgw
badmintoncryer Mar 18, 2026
df895f1
fix: add error code names to ValidationError calls after merging main
badmintoncryer Mar 18, 2026
7715eb1
Merge branch 'main' into regional-natgw
badmintoncryer Apr 2, 2026
58a2f65
Merge branch 'main' into regional-natgw
badmintoncryer Apr 7, 2026
30bf7e5
Merge branch 'main' into regional-natgw
badmintoncryer Apr 8, 2026
2b28003
chore: re-trigger CI
badmintoncryer Apr 8, 2026
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
111 changes: 106 additions & 5 deletions packages/aws-cdk-lib/aws-ec2/lib/nat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Connections, IConnectable } from './connections';
import { CfnNatGateway } from './ec2.generated';
import { Instance } from './instance';
import { InstanceArchitecture, InstanceType } from './instance-types';
import { IKeyPair } from './key-pair';
Expand All @@ -9,7 +10,8 @@ import { ISecurityGroup, SecurityGroup } from './security-group';
import { UserData } from './user-data';
import { PrivateSubnet, PublicSubnet, RouterType, Vpc } from './vpc';
import * as iam from '../../aws-iam';
import { Fn, Token, UnscopedValidationError } from '../../core';
import { Duration, Fn, Token, UnscopedValidationError } from '../../core';
import { IEIPRef } from '../../interfaces/generated/aws-ec2-interfaces.generated';

/**
* Direction of traffic to allow all by default.
Expand Down Expand Up @@ -52,8 +54,6 @@ export interface GatewayConfig {
*
* Determines what type of NAT provider to create, either NAT gateways or NAT
* instance.
*
*
*/
export abstract class NatProvider {
/**
Expand Down Expand Up @@ -100,6 +100,19 @@ export abstract class NatProvider {
return new NatInstanceProviderV2(props);
}

/**
* Use a Regional NAT Gateway to provide NAT services for your VPC
*
* Regional NAT Gateways provide automatic multi-AZ redundancy with a single
* gateway that scales across availability zones. AWS automatically manages
* AZ coverage and EIP allocation.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateways-regional.html
*/
public static regionalGateway(props: RegionalNatGatewayProviderProps = {}): NatProvider {
return new RegionalNatGatewayProvider(props);
}

/**
* Return list of gateways spawned by the provider
*/
Expand Down Expand Up @@ -158,9 +171,41 @@ export interface NatGatewayProps {
}

/**
* Properties for a NAT instance
*
* Properties for a Regional NAT Gateway Provider
*
* Regional NAT Gateways provide automatic multi-AZ redundancy with a single
* gateway that scales across availability zones.
*/
export interface RegionalNatGatewayProviderProps {
/**
* Maximum amount of time to wait before forcibly releasing IP addresses
* if connections are still in progress.
*
* @default Duration.seconds(350)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
readonly maxDrainDuration?: Duration;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Missing [1, 4000] range validation.
  2. Why isn't this part of interface NatGatewayProps as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a validation and maxDrainDuration to NatGatewayProps.


/**
* The allocation ID of the Elastic IP address to use for this NAT gateway.
*
* Cannot be specified together with `eip`.
*
* @default - A new EIP is automatically allocated
*/
readonly allocationId?: string;

/**
* Reference to an existing EIP to use for this NAT gateway.
*
* Cannot be specified together with `allocationId`.
*
* @default - A new EIP is automatically allocated
*/
readonly eip?: IEIPRef;
Comment on lines +237 to +255
Copy link
Copy Markdown
Contributor Author

@badmintoncryer badmintoncryer Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allocationId of the L1 construct can accept not only string but also IEIPRef. I wanted to make L2 similarly accept both of these types.

As implementation approaches, there are two options: (i) define the L2 argument as a union, or (ii) provide separate arguments for string and IEIPRef types. Since I was unsure whether union definitions are allowed in L2 arguments due to any type problem in go language, I adopted approach (ii). I will modify it to implementation (i) if necessary.

(i) union type

redaonly eip?: string | IETPRef;

(ii) separate argument

readonly allocationId?: string;
redaonly eip?: IETPRef;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Union type is against the guidelines.
  2. readonly eip?: CfnEIP seems the right type declaration (CfnEIP)

Copy link
Copy Markdown
Contributor Author

@badmintoncryer badmintoncryer Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to a recent L1 fix, CfnXxx now extends IXxxRef, and all L2 constructs also extend IXxxRef. While there is currently no L2 construct for EIP, if we use IEipRef as the argument type, it will work seamlessly even when an L2 construct is implemented in the future, without requiring any code changes. For this reason, I believe it would be preferable to use IEipRef as the argument type. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Thanks for clarifying!

}

/**
* Properties for a NAT instance
*/
export interface NatInstanceProps {
/**
Expand Down Expand Up @@ -333,6 +378,62 @@ export class NatGatewayProvider extends NatProvider {
}
}

/**
* Provider for Regional NAT Gateways
*
* Regional NAT Gateways provide automatic multi-AZ redundancy with a single gateway that scales across availability zones.
* Unlike zonal NAT gateways, a regional NAT gateway does not require a public subnet and is created at the VPC level.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateways-regional.html
*/
export class RegionalNatGatewayProvider extends NatProvider {
private natGateway?: CfnNatGateway;

constructor(private readonly props: RegionalNatGatewayProviderProps = {}) {
super();

if (this.props.allocationId && this.props.eip) {
throw new UnscopedValidationError(
'Cannot specify both allocationId and eip. Use one or the other.',
);
}
}

public configureNat(options: ConfigureNatOptions) {
this.natGateway = new CfnNatGateway(options.vpc, 'RegionalNatGateway', {
vpcId: options.vpc.vpcId,
availabilityMode: 'regional',
connectivityType: 'public',
allocationId: this.props.allocationId ?? this.props.eip,
maxDrainDurationSeconds: this.props.maxDrainDuration?.toSeconds(),
});

// Add routes to the regional NAT gateway in all private subnets
for (const sub of options.privateSubnets) {
this.configureSubnet(sub);
}
}

public configureSubnet(subnet: PrivateSubnet) {
if (!this.natGateway) {
throw new UnscopedValidationError('Cannot configure subnet before configuring NAT gateway');
}
// All private subnets use the same regional NAT gateway ID
subnet.addRoute('DefaultRoute', {
routerType: RouterType.NAT_GATEWAY,
routerId: this.natGateway.attrNatGatewayId,
enablesInternetConnectivity: true,
});
}

public get configuredGateways(): GatewayConfig[] {
// Regional NAT gateway is a single gateway covering all AZs
return this.natGateway
? [{ az: 'regional', gatewayId: this.natGateway.attrNatGatewayId }]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because configuredGateways() is not called from the CDK code and used only by users, I think it is acceptable to describe ‘regional’ as az.

: [];
}
}

/**
* NAT provider which uses NAT Instances
*
Expand Down
50 changes: 45 additions & 5 deletions packages/aws-cdk-lib/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
Ipv6Addresses,
RequestedSubnet,
} from './ip-addresses';
import { NatProvider } from './nat';
import { NatProvider, RegionalNatGatewayProvider } from './nat';
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
import { SubnetFilter } from './subnet';
import {
Expand Down Expand Up @@ -1655,7 +1655,9 @@ export class Vpc extends VpcBase {
this.subnetConfiguration = ifUndefined(props.subnetConfiguration, defaultSubnet);

const natGatewayPlacement = props.natGatewaySubnets || { subnetType: SubnetType.PUBLIC };
const natGatewayCount = determineNatGatewayCount(props.natGateways, this.subnetConfiguration, this.availabilityZones.length);
const natGatewayCount = determineNatGatewayCount(
props.natGateways, this.subnetConfiguration, this.availabilityZones.length, props.natGatewayProvider,
);

if (this.useIpv6) {
this.ipv6Addresses = props.ipv6Addresses ?? Ipv6Addresses.amazonProvided();
Expand Down Expand Up @@ -1702,6 +1704,23 @@ export class Vpc extends VpcBase {
// if gateways are needed create them
if (natGatewayCount > 0) {
const provider = props.natGatewayProvider || NatProvider.gateway();

// Add warnings for Regional NAT Gateway if unnecessary options are specified
if (provider instanceof RegionalNatGatewayProvider) {
if (props.natGateways !== undefined && props.natGateways !== 1) {
Annotations.of(this).addWarningV2(
'@aws-cdk/aws-ec2:regionalNatGatewayCount',
'natGateways is ignored when using Regional NAT Gateway. A single regional gateway covers all AZs.',
);
}
if (props.natGatewaySubnets !== undefined) {
Annotations.of(this).addWarningV2(
'@aws-cdk/aws-ec2:regionalNatGatewaySubnets',
'natGatewaySubnets is ignored when using Regional NAT Gateway. The gateway is created at VPC level without requiring a public subnet.',
);
}
}

this.createNatGateways(provider, natGatewayCount, natGatewayPlacement);
}
}
Expand Down Expand Up @@ -1797,6 +1816,17 @@ export class Vpc extends VpcBase {
}

private createNatGateways(provider: NatProvider, natCount: number, placement: SubnetSelection): void {
// Regional NAT Gateway does not require public subnets
if (provider instanceof RegionalNatGatewayProvider) {
provider.configureNat({
vpc: this,
natSubnets: [], // Regional NAT Gateway doesn't use natSubnets
privateSubnets: this.privateSubnets as PrivateSubnet[],
});
return;
}

// Zonal NAT Gateway requires public subnets
const natSubnets: PublicSubnet[] = this.selectSubnetObjects(placement) as PublicSubnet[];
for (const sub of natSubnets) {
if (this.publicSubnets.indexOf(sub) === -1) {
Expand Down Expand Up @@ -2787,19 +2817,29 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat
* 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) {
function determineNatGatewayCount(
requestedCount: number | undefined,
subnetConfig: SubnetConfiguration[],
azCount: number,
natGatewayProvider?: NatProvider,
) {
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);
// Regional NAT Gateway uses a single gateway regardless of AZ count
const isRegionalNatGateway = natGatewayProvider instanceof RegionalNatGatewayProvider;
const count = requestedCount !== undefined
? Math.min(requestedCount, azCount)
: (hasPrivateSubnets ? (isRegionalNatGateway ? 1 : azCount) : 0);

if (count === 0 && hasPrivateSubnets && !hasCustomEgress) {
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) {
// Regional NAT Gateway does not require public subnets
if (count > 0 && !hasPublicSubnets && !isRegionalNatGateway) {
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)}.`);
}

Expand Down
Loading
Loading