Skip to content

Commit e1195f9

Browse files
authored
feat(vpcv2): vpc peering connection construct (#31645)
### Issue # (if applicable) Contributes to closing [RFC#507](https://github.com/aws/aws-cdk-rfcs/blob/57fd92a7f20e242b96885264c12567493f5e867f/text/0507-subnets.md?plain=1#L252) Tracking: #30762 ### Reason for this change This PR implements a new L2 construct to setup VPC Peering Connection. ### Description of changes - L2 class(VPCPeeringConnection) - Function `validateVpcCidrOverlap` to ensure IPv4 CIDR blocks don't overlap for subnets in the VPCs ### Description of how you validated changes - Unit tests to test combination of accounts (cross account & cross region, default same account & same region) - Unit tests to see simulate when CIDR blocks overlap - Primary CIDR block overlaps - Secondary CIDR block overlaps - Primary and secondary CIDR block overlaps - Integration test for peering connection ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 326e2c6 commit e1195f9

25 files changed

+1728
-26
lines changed

packages/@aws-cdk/aws-ec2-alpha/README.md

+151
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,159 @@ new Route(this, 'DynamoDBRoute', {
225225
destination: '0.0.0.0/0',
226226
target: { endpoint: dynamoEndpoint },
227227
});
228+
229+
```
230+
231+
## VPC Peering Connection
232+
233+
VPC peering connection allows you to connect two VPCs and route traffic between them using private IP addresses. The VpcV2 construct supports creating VPC peering connections through the `VPCPeeringConnection` construct from the `route` module.
234+
235+
Peering Connection cannot be established between two VPCs with overlapping CIDR ranges. Please make sure the two VPC CIDRs do not overlap with each other else it will throw an error.
236+
237+
For more information, see [What is VPC peering?](https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html).
238+
239+
The following show examples of how to create a peering connection between two VPCs for all possible combinations of same-account or cross-account, and same-region or cross-region configurations.
240+
241+
Note: You cannot create a VPC peering connection between VPCs that have matching or overlapping CIDR blocks
242+
243+
**Case 1: Same Account and Same Region Peering Connection**
244+
245+
```ts
246+
const stack = new Stack();
247+
248+
const vpcA = new VpcV2(this, 'VpcA', {
249+
primaryAddressBlock: IpAddresses.ipv4('10.0.0.0/16'),
250+
});
251+
252+
const vpcB = new VpcV2(this, 'VpcB', {
253+
primaryAddressBlock: IpAddresses.ipv4('10.1.0.0/16'),
254+
});
255+
256+
const peeringConnection = vpcA.createPeeringConnection('sameAccountSameRegionPeering', {
257+
acceptorVpc: vpcB,
258+
});
259+
```
260+
261+
**Case 2: Same Account and Cross Region Peering Connection**
262+
263+
There is no difference from Case 1 when calling `createPeeringConnection`. The only change is that one of the VPCs are created in another stack with a different region. To establish cross region VPC peering connection, acceptorVpc needs to be imported to the requestor VPC stack using `fromVpcV2Attributes` method.
264+
265+
```ts
266+
const app = new App();
267+
268+
const stackA = new Stack(app, 'VpcStackA', { env: { account: '000000000000', region: 'us-east-1' } });
269+
const stackB = new Stack(app, 'VpcStackB', { env: { account: '000000000000', region: 'us-west-2' } });
270+
271+
const vpcA = new VpcV2(stackA, 'VpcA', {
272+
primaryAddressBlock: IpAddresses.ipv4('10.0.0.0/16'),
273+
});
274+
275+
new VpcV2(stackB, 'VpcB', {
276+
primaryAddressBlock: IpAddresses.ipv4('10.1.0.0/16'),
277+
});
278+
279+
const vpcB = VpcV2.fromVpcV2Attributes(stackA, 'ImportedVpcB', {
280+
vpcId: 'MockVpcBid',
281+
vpcCidrBlock: '10.1.0.0/16',
282+
region: 'us-west-2',
283+
ownerAccountId: '000000000000',
284+
});
285+
286+
287+
const peeringConnection = vpcA.createPeeringConnection('sameAccountCrossRegionPeering', {
288+
acceptorVpc: vpcB,
289+
});
228290
```
229291

292+
**Case 3: Cross Account Peering Connection**
293+
294+
For cross-account connections, the acceptor account needs an IAM role that grants the requestor account permission to initiate the connection. Create a new IAM role in the acceptor account using method `createAcceptorVpcRole` to provide the necessary permissions.
295+
296+
Once role is created in account, provide role arn for field `peerRoleArn` under method `createPeeringConnection`
297+
298+
```ts
299+
const stack = new Stack();
300+
301+
const acceptorVpc = new VpcV2(this, 'VpcA', {
302+
primaryAddressBlock: IpAddresses.ipv4('10.0.0.0/16'),
303+
});
304+
305+
const acceptorRoleArn = acceptorVpc.createAcceptorVpcRole('000000000000'); // Requestor account ID
306+
```
307+
308+
After creating an IAM role in the acceptor account, we can initiate the peering connection request from the requestor VPC. Import accpeptorVpc to the stack using `fromVpcV2Attributes` method, it is recommended to specify owner account id of the acceptor VPC in case of cross account peering connection, if acceptor VPC is hosted in different region provide region value for import as well.
309+
The following code snippet demonstrates how to set up VPC peering between two VPCs in different AWS accounts using CDK:
310+
311+
```ts
312+
const stack = new Stack();
313+
314+
const acceptorVpc = VpcV2.fromVpcV2Attributes(this, 'acceptorVpc', {
315+
vpcId: 'vpc-XXXX',
316+
vpcCidrBlock: '10.0.0.0/16',
317+
region: 'us-east-2',
318+
ownerAccountId: '111111111111',
319+
});
320+
321+
const acceptorRoleArn = 'arn:aws:iam::111111111111:role/VpcPeeringRole';
322+
323+
const requestorVpc = new VpcV2(this, 'VpcB', {
324+
primaryAddressBlock: IpAddresses.ipv4('10.1.0.0/16'),
325+
});
326+
327+
const peeringConnection = requestorVpc.createPeeringConnection('crossAccountCrossRegionPeering', {
328+
acceptorVpc: acceptorVpc,
329+
peerRoleArn: acceptorRoleArn,
330+
});
331+
```
332+
333+
### Route Table Configuration
334+
335+
After establishing the VPC peering connection, routes must be added to the respective route tables in the VPCs to enable traffic flow. If a route is added to the requestor stack, information will be able to flow from the requestor VPC to the acceptor VPC, but not in the reverse direction. For bi-directional communication, routes need to be added in both VPCs from their respective stacks.
336+
337+
For more information, see [Update your route tables for a VPC peering connection](https://docs.aws.amazon.com/vpc/latest/peering/vpc-peering-routing.html).
338+
339+
```ts
340+
const stack = new Stack();
341+
342+
const acceptorVpc = new VpcV2(this, 'VpcA', {
343+
primaryAddressBlock: IpAddresses.ipv4('10.0.0.0/16'),
344+
});
345+
346+
const requestorVpc = new VpcV2(this, 'VpcB', {
347+
primaryAddressBlock: IpAddresses.ipv4('10.1.0.0/16'),
348+
});
349+
350+
const peeringConnection = requestorVpc.createPeeringConnection('peeringConnection', {
351+
acceptorVpc: acceptorVpc,
352+
});
353+
354+
const routeTable = new RouteTable(this, 'RouteTable', {
355+
vpc: requestorVpc,
356+
});
357+
358+
routeTable.addRoute('vpcPeeringRoute', '10.0.0.0/16', { gateway: peeringConnection });
359+
```
360+
361+
This can also be done using AWS CLI. For more information, see [create-route](https://docs.aws.amazon.com/cli/latest/reference/ec2/create-route.html).
362+
363+
```bash
364+
# Add a route to the requestor VPC route table
365+
aws ec2 create-route --route-table-id rtb-requestor --destination-cidr-block 10.0.0.0/16 --vpc-peering-connection-id pcx-xxxxxxxx
366+
367+
# For bi-directional add a route in the acceptor vpc account as well
368+
aws ec2 create-route --route-table-id rtb-acceptor --destination-cidr-block 10.1.0.0/16 --vpc-peering-connection-id pcx-xxxxxxxx
369+
```
370+
371+
### Deleting the Peering Connection
372+
373+
To delete a VPC peering connection, use the following command:
374+
375+
```bash
376+
aws ec2 delete-vpc-peering-connection --vpc-peering-connection-id pcx-xxxxxxxx
377+
```
378+
379+
For more information, see [Delete a VPC peering connection](https://docs.aws.amazon.com/vpc/latest/peering/create-vpc-peering-connection.html#delete-vpc-peering-connection).
380+
230381
## Adding Egress-Only Internet Gateway to VPC
231382

232383
An egress-only internet gateway is a horizontally scaled, redundant, and highly available VPC component that allows outbound communication over IPv6 from instances in your VPC to the internet, and prevents the internet from initiating an IPv6 connection with your instances.

packages/@aws-cdk/aws-ec2-alpha/lib/route.ts

+135-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { CfnEIP, CfnEgressOnlyInternetGateway, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnRouteTable, CfnVPCGatewayAttachment, CfnVPNGateway, CfnVPNGatewayRoutePropagation, GatewayVpcEndpoint, IRouteTable, IVpcEndpoint, RouterType } from 'aws-cdk-lib/aws-ec2';
1+
import { CfnEIP, CfnEgressOnlyInternetGateway, CfnInternetGateway, CfnNatGateway, CfnVPCPeeringConnection, CfnRoute, CfnRouteTable, CfnVPCGatewayAttachment, CfnVPNGateway, CfnVPNGatewayRoutePropagation, GatewayVpcEndpoint, IRouteTable, IVpcEndpoint, RouterType } from 'aws-cdk-lib/aws-ec2';
22
import { Construct, IDependable } from 'constructs';
33
import { Annotations, Duration, IResource, Resource } from 'aws-cdk-lib/core';
44
import { IVpcV2, VPNGatewayV2Options } from './vpc-v2-base';
5-
import { NetworkUtils, allRouteTableIds } from './util';
5+
import { NetworkUtils, allRouteTableIds, CidrBlock } from './util';
66
import { ISubnetV2 } from './subnet-v2';
77

88
/**
@@ -175,6 +175,40 @@ export interface NatGatewayProps extends NatGatewayOptions {
175175
readonly vpc?: IVpcV2;
176176
}
177177

178+
/**
179+
* Options to define a VPC peering connection.
180+
*/
181+
export interface VPCPeeringConnectionOptions {
182+
/**
183+
* The VPC that is accepting the peering connection.
184+
*/
185+
readonly acceptorVpc: IVpcV2;
186+
187+
/**
188+
* The role arn created in the acceptor account.
189+
*
190+
* @default - no peerRoleArn needed if not cross account connection
191+
*/
192+
readonly peerRoleArn?: string;
193+
194+
/**
195+
* The resource name of the peering connection.
196+
*
197+
* @default - peering connection provisioned without any name
198+
*/
199+
readonly vpcPeeringConnectionName?: string;
200+
}
201+
202+
/**
203+
* Properties to define a VPC peering connection.
204+
*/
205+
export interface VPCPeeringConnectionProps extends VPCPeeringConnectionOptions {
206+
/**
207+
* The VPC that is requesting the peering connection.
208+
*/
209+
readonly requestorVpc: IVpcV2;
210+
}
211+
178212
/**
179213
* Creates an egress-only internet gateway
180214
* @resource AWS::EC2::EgressOnlyInternetGateway
@@ -312,7 +346,7 @@ export class VPNGatewayV2 extends Resource implements IRouteTarget {
312346
const routeTableIds = allRouteTableIds(subnets);
313347

314348
if (routeTableIds.length === 0) {
315-
Annotations.of(this).addWarningV2('@aws-cdk:aws-ec2-elpha:enableVpnGatewayV2', `No subnets matching selection: '${JSON.stringify(vpnRoutePropagation)}'. Select other subnets to add routes to.`);
349+
Annotations.of(scope).addWarningV2('@aws-cdk:aws-ec2-elpha:enableVpnGatewayV2', `No subnets matching selection: '${JSON.stringify(vpnRoutePropagation)}'. Select other subnets to add routes to.`);
316350
}
317351

318352
this._routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', {
@@ -402,6 +436,103 @@ export class NatGateway extends Resource implements IRouteTarget {
402436
}
403437
}
404438

439+
/**
440+
* Creates a peering connection between two VPCs
441+
* @resource AWS::EC2::VPCPeeringConnection
442+
*/
443+
export class VPCPeeringConnection extends Resource implements IRouteTarget {
444+
445+
/**
446+
* The type of router used in the route.
447+
*/
448+
readonly routerType: RouterType;
449+
450+
/**
451+
* The ID of the route target.
452+
*/
453+
readonly routerTargetId: string;
454+
455+
/**
456+
* The VPC peering connection CFN resource.
457+
*/
458+
public readonly resource: CfnVPCPeeringConnection;
459+
460+
constructor(scope: Construct, id: string, props: VPCPeeringConnectionProps) {
461+
super(scope, id);
462+
463+
this.routerType = RouterType.VPC_PEERING_CONNECTION;
464+
465+
const isCrossAccount = props.requestorVpc.ownerAccountId !== props.acceptorVpc.ownerAccountId;
466+
467+
if (!isCrossAccount && props.peerRoleArn) {
468+
throw new Error('peerRoleArn is not needed for same account peering');
469+
}
470+
471+
if (isCrossAccount && !props.peerRoleArn) {
472+
throw new Error('Cross account VPC peering requires peerRoleArn');
473+
}
474+
475+
const overlap = this.validateVpcCidrOverlap(props.requestorVpc, props.acceptorVpc);
476+
if (overlap) {
477+
throw new Error('CIDR block should not overlap with each other for establishing a peering connection');
478+
}
479+
480+
this.resource = new CfnVPCPeeringConnection(this, 'VPCPeeringConnection', {
481+
vpcId: props.requestorVpc.vpcId,
482+
peerVpcId: props.acceptorVpc.vpcId,
483+
peerOwnerId: props.acceptorVpc.ownerAccountId,
484+
peerRegion: props.acceptorVpc.region,
485+
peerRoleArn: isCrossAccount ? props.peerRoleArn : undefined,
486+
});
487+
488+
this.routerTargetId = this.resource.attrId;
489+
this.node.defaultChild = this.resource;
490+
}
491+
492+
/**
493+
* Validates if the provided IPv4 CIDR block overlaps with existing subnet CIDR blocks within the given VPC.
494+
*
495+
* @param requestorVpc The VPC of the requestor.
496+
* @param acceptorVpc The VPC of the acceptor.
497+
* @returns True if the IPv4 CIDR block overlaps with each other for two VPCs, false otherwise.
498+
* @internal
499+
*/
500+
private validateVpcCidrOverlap(requestorVpc: IVpcV2, acceptorVpc: IVpcV2): boolean {
501+
502+
const requestorCidrs = [requestorVpc.ipv4CidrBlock];
503+
const acceptorCidrs = [acceptorVpc.ipv4CidrBlock];
504+
505+
if (requestorVpc.secondaryCidrBlock) {
506+
requestorCidrs.push(...requestorVpc.secondaryCidrBlock
507+
.map(block => block.cidrBlock)
508+
.filter((cidr): cidr is string => cidr !== undefined));
509+
}
510+
511+
if (acceptorVpc.secondaryCidrBlock) {
512+
acceptorCidrs.push(...acceptorVpc.secondaryCidrBlock
513+
.map(block => block.cidrBlock)
514+
.filter((cidr): cidr is string => cidr !== undefined));
515+
}
516+
517+
for (const requestorCidr of requestorCidrs) {
518+
const requestorRange = new CidrBlock(requestorCidr);
519+
const requestorIpRange: [string, string] = [requestorRange.minIp(), requestorRange.maxIp()];
520+
521+
for (const acceptorCidr of acceptorCidrs) {
522+
const acceptorRange = new CidrBlock(acceptorCidr);
523+
const acceptorIpRange: [string, string] = [acceptorRange.minIp(), acceptorRange.maxIp()];
524+
525+
if (requestorRange.rangesOverlap(acceptorIpRange, requestorIpRange)) {
526+
return true;
527+
}
528+
}
529+
}
530+
531+
return false;
532+
}
533+
534+
}
535+
405536
/**
406537
* The type of endpoint or gateway being targeted by the route.
407538
*/
@@ -534,7 +665,7 @@ export class Route extends Resource implements IRouteV2 {
534665
/**
535666
* The type of router the route is targetting
536667
*/
537-
public readonly targetRouterType: RouterType
668+
public readonly targetRouterType: RouterType;
538669

539670
/**
540671
* The route CFN resource.

packages/@aws-cdk/aws-ec2-alpha/lib/util.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -378,5 +378,4 @@ export class CidrBlockIpv6 {
378378
}
379379
return ipv6Number;
380380
}
381-
}
382-
381+
}

0 commit comments

Comments
 (0)