Ping Me! (Part 1: VPC Peering Using CDK)
Rafal Krol
Posted on July 24, 2021
Overview
VPC Peering is a networking connection that you can establish between two VPCs to allow instances on either end to communicate with each other, using their private IPs (both IPv4 and IPv6 are supported), in exactly the same way as if they were inside one VPC. Traffic never leaves the AWS backbone network, thus avoiding the dirty pipes of the Internet. The connection itself is free of charge. However, you have to pay for the data transferred between the VPCs.
Intra-region (between VPCs within the same region), inter-region (between VPCs in different regions) and cross-account (between VPCs belonging to different AWS accounts) peering are all possible. Of course, in either case, the CIDR ranges of the peered VPCs mustn't overlap with each other, e.g. peering VPC A with the CIDR range of 10.0.0.0/16 and VPC B with the CIDR range of 10.0.1.0/24 would not be possible as IP addresses from 10.0.1.0 to 10.0.1.255 exist in both VPCs.
One more gotcha is that transitive peering is also disallowed. Hence, if you got VPC A peered to VPC B and VPC B peered to VPC C, you wouldn't be able to reach VPC C from VPC A through VPC B. Instead, you'd need to peer VPC A directly with VPC C. With three VPCs it shouldn't be such a hard thing to accomplish (and then to maintain), but imagine having hundreds of VPCs... To achieve full mesh topology in that scenario, you'd need a Transit Gateway. But I'm getting a little ahead of myself.
Implementation
We'll need three stacks: one for the two VPCs, another one for the two EC2 instances and the third one for the actual peering connection and appropriate routes.
Let's begin by creating the file for our VpcStack
class:
➜ ping-me-cdk-example$ touch lib/vpc.ts
Now, the class itself:
ping-me-cdk-example/lib/vpc.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2'; // <--- this module is not available from the start; remember to import it: `npm install @aws-cdk/aws-ec2`
interface VpcProps extends cdk.StackProps {
vpcSetup: {
cidrs: string[], // <--- each VPC will need a list of CIDRs
maxAzs?: number, // <--- optionally the number of Availability Zones can be provided; defaults to 2 in our particular case
vpnConnections?: { // <--- if dealing with Site-to-Site VPN, the VPN connection details can be provided
[id: string]: ec2.VpnConnectionOptions;
},
};
}
export class VpcStack extends cdk.Stack {
readonly createdVpcs: ec2.Vpc[]; // <-- create a class property for exposing the list of VPC objects
constructor(scope: cdk.Construct, id: string, props: VpcProps) {
super(scope, id, props);
const createdVpcs: ec2.Vpc[] = [];
// for each of the provided CIDR ranges, create a VPC with two /27 subnets (one public and one private) per AZ
props.vpcSetup.cidrs.forEach((cidr, index) => {
createdVpcs.push(new ec2.Vpc(this, 'Vpc' + index, {
cidr,
maxAzs: props.vpcSetup.maxAzs,
subnetConfiguration: [
{
cidrMask: 27,
name: 'public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 27,
name: 'private',
subnetType: ec2.SubnetType.PRIVATE,
},
],
vpnConnections: props.vpcSetup.vpnConnections,
}));
});
// For each VPC's default security group, allow inbound ICMP (ping) requests from anywhere
createdVpcs.forEach((vpc, index) => {
ec2.SecurityGroup.fromSecurityGroupId(this, 'DefaultSecurityGroup' + index, vpc.vpcDefaultSecurityGroup)
.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.icmpPing(), 'Allow ping from anywhere');
});
this.createdVpcs = createdVpcs; // <-- expose the list of created VPC objects so that they can be used by different stacks
}
}
Since the @aws-cdk/aws-ec2
module was not imported during the cdk initialization, let's install it now:
➜ ping-me-cdk-example$ npm install @aws-cdk/aws-ec2@1.73.0
npm WARN @aws-cdk/aws-ec2@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-ec2@1.74.0 requires a peer of @aws-cdk/cx-api@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-ec2@1.74.0 requires a peer of @aws-cdk/cloud-assembly-schema@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-ec2@1.74.0 requires a peer of @aws-cdk/region-info@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-cloudwatch@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-ssm@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-ssm@1.74.0 requires a peer of @aws-cdk/cloud-assembly-schema@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-logs@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-s3-assets@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-s3-assets@1.74.0 requires a peer of @aws-cdk/cx-api@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-kms@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-s3@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-iam@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-iam@1.74.0 requires a peer of @aws-cdk/region-info@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/assets@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/assets@1.74.0 requires a peer of @aws-cdk/cx-api@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/aws-events@1.74.0 requires a peer of @aws-cdk/core@1.74.0 but none is installed. You must install peer dependencies yourself.
npm WARN ping-me-cdk-example@0.1.0 No repository field.
npm WARN ping-me-cdk-example@0.1.0 No license field.
+ @aws-cdk/aws-ec2@1.74.0
added 190 packages from 9 contributors and audited 932 packages in 10.624s
27 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
We chose the 1.73.0
version on purpose (the exact same one we used for our @aws-cdk/core
module) to avoid the possibility of seeing the Argument of type 'this' is not assignable to parameter of type 'Construct'
error.
Next, we'll initialize an instance of our VpcStack
class in ping-me-cdk-example/bin/ping-me-cdk-example.ts
:
ping-me-cdk-example/bin/ping-me-cdk-example.ts
import * as cdk from '@aws-cdk/core';
import { VpcStack } from '../lib/vpc';
const app = new cdk.App(); // <--- you can read more about the App construct here: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.App.html
const vpcPeers = new VpcStack(app, 'VpcPeersStack', {
vpcSetup: {
cidrs: ['10.0.0.0/24', '10.0.1.0/24'], // <--- two non-overlapping CIDR ranges for our two VPCs
maxAzs: 1, // <--- to keep the costs down, we'll stick to 1 availability zone per VPC (obviously, not something you'd want to do in production)
},
});
TypeScript should be compiled to JavaScript after each modification to our source code. To avoid manually executing the npm run build
command every time that happens, we'll run the below:
➜ ping-me-cdk-example$ npm run watch
[19:50:46] Starting compilation in watch mode...
[19:50:51] Found 0 errors. Watching for file changes.
# KEEP THIS RUNNING!
We're ready to synthesize our code into a CloudFormation template. As this is an optional step, we shall do it now for the sake of demonstration, but refrain from doing it later on:
➜ ping-me-cdk-example$ cdk synth
Resources:
Vpc07C831B30:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/24
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/Resource
Vpc0publicSubnet1SubnetB977A71E:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/27
VpcId:
Ref: Vpc07C831B30
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: true
Tags:
- Key: aws-cdk:subnet-name
Value: public
- Key: aws-cdk:subnet-type
Value: Public
- Key: Name
Value: VpcPeersStack/Vpc0/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/Subnet
Vpc0publicSubnet1RouteTable2012E33A:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: Vpc07C831B30
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/RouteTable
Vpc0publicSubnet1RouteTableAssociation0E1C3D4B:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: Vpc0publicSubnet1RouteTable2012E33A
SubnetId:
Ref: Vpc0publicSubnet1SubnetB977A71E
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/RouteTableAssociation
Vpc0publicSubnet1DefaultRouteC03283FF:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: Vpc0publicSubnet1RouteTable2012E33A
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: Vpc0IGW3080DF7F
DependsOn:
- Vpc0VPCGW9FBA9469
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/DefaultRoute
Vpc0publicSubnet1EIP16FED7DC:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/EIP
Vpc0publicSubnet1NATGateway40294DF4:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- Vpc0publicSubnet1EIP16FED7DC
- AllocationId
SubnetId:
Ref: Vpc0publicSubnet1SubnetB977A71E
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/publicSubnet1/NATGateway
Vpc0privateSubnet1SubnetD6383522:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.32/27
VpcId:
Ref: Vpc07C831B30
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
Tags:
- Key: aws-cdk:subnet-name
Value: private
- Key: aws-cdk:subnet-type
Value: Private
- Key: Name
Value: VpcPeersStack/Vpc0/privateSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/privateSubnet1/Subnet
Vpc0privateSubnet1RouteTableB5C6777D:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: Vpc07C831B30
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0/privateSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/privateSubnet1/RouteTable
Vpc0privateSubnet1RouteTableAssociationC17661A1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: Vpc0privateSubnet1RouteTableB5C6777D
SubnetId:
Ref: Vpc0privateSubnet1SubnetD6383522
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/privateSubnet1/RouteTableAssociation
Vpc0privateSubnet1DefaultRoute1EA0AEFE:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: Vpc0privateSubnet1RouteTableB5C6777D
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: Vpc0publicSubnet1NATGateway40294DF4
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/privateSubnet1/DefaultRoute
Vpc0IGW3080DF7F:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: VpcPeersStack/Vpc0
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/IGW
Vpc0VPCGW9FBA9469:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: Vpc07C831B30
InternetGatewayId:
Ref: Vpc0IGW3080DF7F
Metadata:
aws:cdk:path: VpcPeersStack/Vpc0/VPCGW
Vpc1C211860B:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.1.0/24
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/Resource
Vpc1publicSubnet1SubnetB43EFACE:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/27
VpcId:
Ref: Vpc1C211860B
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: true
Tags:
- Key: aws-cdk:subnet-name
Value: public
- Key: aws-cdk:subnet-type
Value: Public
- Key: Name
Value: VpcPeersStack/Vpc1/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/Subnet
Vpc1publicSubnet1RouteTable1C630681:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: Vpc1C211860B
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/RouteTable
Vpc1publicSubnet1RouteTableAssociation4DA13984:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: Vpc1publicSubnet1RouteTable1C630681
SubnetId:
Ref: Vpc1publicSubnet1SubnetB43EFACE
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/RouteTableAssociation
Vpc1publicSubnet1DefaultRouteB4C85D62:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: Vpc1publicSubnet1RouteTable1C630681
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: Vpc1IGW15AE5E6B
DependsOn:
- Vpc1VPCGW4C1BD07A
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/DefaultRoute
Vpc1publicSubnet1EIP5F1D9658:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/EIP
Vpc1publicSubnet1NATGateway06106699:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- Vpc1publicSubnet1EIP5F1D9658
- AllocationId
SubnetId:
Ref: Vpc1publicSubnet1SubnetB43EFACE
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1/publicSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/publicSubnet1/NATGateway
Vpc1privateSubnet1Subnet41967AFD:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.32/27
VpcId:
Ref: Vpc1C211860B
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
Tags:
- Key: aws-cdk:subnet-name
Value: private
- Key: aws-cdk:subnet-type
Value: Private
- Key: Name
Value: VpcPeersStack/Vpc1/privateSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/privateSubnet1/Subnet
Vpc1privateSubnet1RouteTable339A93B3:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: Vpc1C211860B
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1/privateSubnet1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/privateSubnet1/RouteTable
Vpc1privateSubnet1RouteTableAssociation4FB53340:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: Vpc1privateSubnet1RouteTable339A93B3
SubnetId:
Ref: Vpc1privateSubnet1Subnet41967AFD
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/privateSubnet1/RouteTableAssociation
Vpc1privateSubnet1DefaultRoute4ACBA7B3:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: Vpc1privateSubnet1RouteTable339A93B3
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: Vpc1publicSubnet1NATGateway06106699
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/privateSubnet1/DefaultRoute
Vpc1IGW15AE5E6B:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: VpcPeersStack/Vpc1
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/IGW
Vpc1VPCGW4C1BD07A:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: Vpc1C211860B
InternetGatewayId:
Ref: Vpc1IGW15AE5E6B
Metadata:
aws:cdk:path: VpcPeersStack/Vpc1/VPCGW
DefaultSecurityGroup0from00000ICMPType829E2C81F:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: icmp
CidrIp: 0.0.0.0/0
Description: Allow ping from anywhere
FromPort: 8
GroupId:
Fn::GetAtt:
- Vpc07C831B30
- DefaultSecurityGroup
ToPort: -1
Metadata:
aws:cdk:path: VpcPeersStack/DefaultSecurityGroup0/from 0.0.0.0_0:ICMP Type 8
DefaultSecurityGroup1from00000ICMPType8D69AB703:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: icmp
CidrIp: 0.0.0.0/0
Description: Allow ping from anywhere
FromPort: 8
GroupId:
Fn::GetAtt:
- Vpc1C211860B
- DefaultSecurityGroup
ToPort: -1
Metadata:
aws:cdk:path: VpcPeersStack/DefaultSecurityGroup1/from 0.0.0.0_0:ICMP Type 8
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Modules: aws-cdk=1.74.0,@aws-cdk/assets=1.74.0,@aws-cdk/aws-cloudwatch=1.74.0,@aws-cdk/aws-ec2=1.74.0,@aws-cdk/aws-events=1.74.0,@aws-cdk/aws-iam=1.74.0,@aws-cdk/aws-kms=1.74.0,@aws-cdk/aws-logs=1.74.0,@aws-cdk/aws-s3=1.74.0,@aws-cdk/aws-s3-assets=1.74.0,@aws-cdk/aws-ssm=1.74.0,@aws-cdk/cloud-assembly-schema=1.74.0,@aws-cdk/core=1.74.0,@aws-cdk/cx-api=1.74.0,@aws-cdk/region-info=1.74.0,jsii-runtime=node.js/v14.14.0
Metadata:
aws:cdk:path: VpcPeersStack/CDKMetadata/Default
Condition: CDKMetadataAvailable
Conditions:
CDKMetadataAvailable:
Fn::Or:
- Fn::Or:
- Fn::Equals:
- Ref: AWS::Region
- ap-east-1
- Fn::Equals:
- Ref: AWS::Region
- ap-northeast-1
- Fn::Equals:
- Ref: AWS::Region
- ap-northeast-2
- Fn::Equals:
- Ref: AWS::Region
- ap-south-1
- Fn::Equals:
- Ref: AWS::Region
- ap-southeast-1
- Fn::Equals:
- Ref: AWS::Region
- ap-southeast-2
- Fn::Equals:
- Ref: AWS::Region
- ca-central-1
- Fn::Equals:
- Ref: AWS::Region
- cn-north-1
- Fn::Equals:
- Ref: AWS::Region
- cn-northwest-1
- Fn::Equals:
- Ref: AWS::Region
- eu-central-1
- Fn::Or:
- Fn::Equals:
- Ref: AWS::Region
- eu-north-1
- Fn::Equals:
- Ref: AWS::Region
- eu-west-1
- Fn::Equals:
- Ref: AWS::Region
- eu-west-2
- Fn::Equals:
- Ref: AWS::Region
- eu-west-3
- Fn::Equals:
- Ref: AWS::Region
- me-south-1
- Fn::Equals:
- Ref: AWS::Region
- sa-east-1
- Fn::Equals:
- Ref: AWS::Region
- us-east-1
- Fn::Equals:
- Ref: AWS::Region
- us-east-2
- Fn::Equals:
- Ref: AWS::Region
- us-west-1
- Fn::Equals:
- Ref: AWS::Region
- us-west-2
Yep, yep, Ladies and Gentleman, without the CDK we would be forced to write all of the above lines ourselves if we wanted to deploy our infrastructure with CloudFormation (that's one giant leap right there).
Instead of looking at the CloudFormation template, you can run the cdk diff
command to see what changes can be applied:
➜ ping-me-cdk-example$ cdk diff
Stack VpcPeersStack
Security Group Changes
┌───┬──────────────────────────────┬─────┬───────────┬─────────────────┐
│ │ Group │ Dir │ Protocol │ Peer │
├───┼──────────────────────────────┼─────┼───────────┼─────────────────┤
│ + │ ${Vpc0.DefaultSecurityGroup} │ In │ ICMP 8--1 │ Everyone (IPv4) │
├───┼──────────────────────────────┼─────┼───────────┼─────────────────┤
│ + │ ${Vpc1.DefaultSecurityGroup} │ In │ ICMP 8--1 │ Everyone (IPv4) │
└───┴──────────────────────────────┴─────┴───────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}
Resources
[+] AWS::EC2::VPC Vpc0 Vpc07C831B30
[+] AWS::EC2::Subnet Vpc0/publicSubnet1/Subnet Vpc0publicSubnet1SubnetB977A71E
[+] AWS::EC2::RouteTable Vpc0/publicSubnet1/RouteTable Vpc0publicSubnet1RouteTable2012E33A
[+] AWS::EC2::SubnetRouteTableAssociation Vpc0/publicSubnet1/RouteTableAssociation Vpc0publicSubnet1RouteTableAssociation0E1C3D4B
[+] AWS::EC2::Route Vpc0/publicSubnet1/DefaultRoute Vpc0publicSubnet1DefaultRouteC03283FF
[+] AWS::EC2::EIP Vpc0/publicSubnet1/EIP Vpc0publicSubnet1EIP16FED7DC
[+] AWS::EC2::NatGateway Vpc0/publicSubnet1/NATGateway Vpc0publicSubnet1NATGateway40294DF4
[+] AWS::EC2::Subnet Vpc0/privateSubnet1/Subnet Vpc0privateSubnet1SubnetD6383522
[+] AWS::EC2::RouteTable Vpc0/privateSubnet1/RouteTable Vpc0privateSubnet1RouteTableB5C6777D
[+] AWS::EC2::SubnetRouteTableAssociation Vpc0/privateSubnet1/RouteTableAssociation Vpc0privateSubnet1RouteTableAssociationC17661A1
[+] AWS::EC2::Route Vpc0/privateSubnet1/DefaultRoute Vpc0privateSubnet1DefaultRoute1EA0AEFE
[+] AWS::EC2::InternetGateway Vpc0/IGW Vpc0IGW3080DF7F
[+] AWS::EC2::VPCGatewayAttachment Vpc0/VPCGW Vpc0VPCGW9FBA9469
[+] AWS::EC2::VPC Vpc1 Vpc1C211860B
[+] AWS::EC2::Subnet Vpc1/publicSubnet1/Subnet Vpc1publicSubnet1SubnetB43EFACE
[+] AWS::EC2::RouteTable Vpc1/publicSubnet1/RouteTable Vpc1publicSubnet1RouteTable1C630681
[+] AWS::EC2::SubnetRouteTableAssociation Vpc1/publicSubnet1/RouteTableAssociation Vpc1publicSubnet1RouteTableAssociation4DA13984
[+] AWS::EC2::Route Vpc1/publicSubnet1/DefaultRoute Vpc1publicSubnet1DefaultRouteB4C85D62
[+] AWS::EC2::EIP Vpc1/publicSubnet1/EIP Vpc1publicSubnet1EIP5F1D9658
[+] AWS::EC2::NatGateway Vpc1/publicSubnet1/NATGateway Vpc1publicSubnet1NATGateway06106699
[+] AWS::EC2::Subnet Vpc1/privateSubnet1/Subnet Vpc1privateSubnet1Subnet41967AFD
[+] AWS::EC2::RouteTable Vpc1/privateSubnet1/RouteTable Vpc1privateSubnet1RouteTable339A93B3
[+] AWS::EC2::SubnetRouteTableAssociation Vpc1/privateSubnet1/RouteTableAssociation Vpc1privateSubnet1RouteTableAssociation4FB53340
[+] AWS::EC2::Route Vpc1/privateSubnet1/DefaultRoute Vpc1privateSubnet1DefaultRoute4ACBA7B3
[+] AWS::EC2::InternetGateway Vpc1/IGW Vpc1IGW15AE5E6B
[+] AWS::EC2::VPCGatewayAttachment Vpc1/VPCGW Vpc1VPCGW4C1BD07A
[+] AWS::EC2::SecurityGroupIngress DefaultSecurityGroup0/from 0.0.0.0_0:ICMP Type 8 DefaultSecurityGroup0from00000ICMPType829E2C81F
[+] AWS::EC2::SecurityGroupIngress DefaultSecurityGroup1/from 0.0.0.0_0:ICMP Type 8 DefaultSecurityGroup1from00000ICMPType8D69AB703
All looks good. Hence, without further ado, let's deploy these changes:
➜ ping-me-cdk-example$ cdk deploy --require-approval never
VpcPeersStack: deploying...
VpcPeersStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (30/30)
✅ VpcPeersStack
Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcPeersStack/1057bae0-2cc0-11eb-8cd5-0a517997c0b3
We've set the --require-approval
flag to never
to avoid manually confirming the creation of the Allow ping from anywhere
rules, which were deemed as potentially insecure by the CDK.
We got the VPCs. Now, on to the EC2s and the peering connection itself:
➜ ping-me-cdk-example$ touch lib/instance.ts
ping-me-cdk-example/lib/instance.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
interface InstanceProps extends cdk.StackProps {
vpcs: ec2.Vpc[]; // <--- a list of VPC objects required for the creation of the EC2 instance(s)
}
export class InstanceStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: InstanceProps) {
super(scope, id, props);
// For each supplied VPC, create a Linux-based EC2 instance in the private subnet and attach the VPC's default security group to it
props.vpcs.forEach((vpc, index) => {
const instanceName = `Instance${index}`;
const instanceResource = new ec2.BastionHostLinux(this, instanceName, {
vpc,
instanceName,
securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, instanceName + 'SecurityGroup', vpc.vpcDefaultSecurityGroup),
});
// Output the instance's private IP
new cdk.CfnOutput(this, instanceName + 'PrivateIp', {
value: instanceResource.instancePrivateIp,
});
});
}
}
➜ ping-me-cdk-example$ touch lib/peering.ts
ping-me-cdk-example/lib/peering.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
interface PeeringProps extends cdk.StackProps {
vpcs: [ec2.Vpc, ec2.Vpc]; // <--- a fixed-length array (a tuple type in TypeScript parlance) consisting of two VPC objects between which the peering connection will be made
}
export class PeeringStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: PeeringProps) {
super(scope, id, props);
// Create the peering connection
const peer = new ec2.CfnVPCPeeringConnection(this, 'Peer', {
vpcId: props.vpcs[0].vpcId,
peerVpcId: props.vpcs[1].vpcId
});
// Add route from the private subnet of the first VPC to the second VPC over the peering connection
// NB the below was taken from: https://stackoverflow.com/questions/62525195/adding-entry-to-route-table-with-cdk-typescript-when-its-private-subnet-alread
props.vpcs[0].privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
new ec2.CfnRoute(this, 'RouteFromPrivateSubnetOfVpc1ToVpc2' + index, {
destinationCidrBlock: props.vpcs[1].vpcCidrBlock,
routeTableId,
vpcPeeringConnectionId: peer.ref,
});
});
// Add route from the private subnet of the second VPC to the first VPC over the peering connection
props.vpcs[1].privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
new ec2.CfnRoute(this, 'RouteFromPrivateSubnetOfVpc2ToVpc1' + index, {
destinationCidrBlock: props.vpcs[0].vpcCidrBlock,
routeTableId,
vpcPeeringConnectionId: peer.ref,
});
});
}
}
Back to the ping-me-cdk-example/bin/ping-me-cdk-example.ts
file to initialize our newly created classes:
ping-me-cdk-example/bin/ping-me-cdk-example.ts
import * as cdk from '@aws-cdk/core';
import { VpcStack } from '../lib/vpc';
import { InstanceStack } from '../lib/instance';
import { PeeringStack } from '../lib/peering';
const app = new cdk.App(); // <--- you can read more about the App construct here: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.App.html
const vpcPeers = new VpcStack(app, 'VpcPeersStack', {
vpcSetup: {
cidrs: ['10.0.0.0/24', '10.0.1.0/24'], // <--- two non-overlapping CIDR ranges for our two VPCs
maxAzs: 1, // <--- to keep the costs down, we'll stick to 1 availability zone per VPC (obviously, not something you'd want to do in production)
},
});
// Create two EC2 instances, one in each VPC
new InstanceStack(app, 'InstancePeersStack', {
vpcs: vpcPeers.createdVpcs,
});
// Establish a VPC Peering connection between the two VPCs
new PeeringStack(app, 'PeeringStack', {
vpcs: [vpcPeers.createdVpcs[0], vpcPeers.createdVpcs[1]],
});
Finally, we can deploy the two EC2 instances (one in each of the earlier created VPCs) and the VPC Peering connection itself:
➜ ping-me-cdk-example$ cdk deploy --all --require-approval never
VpcPeersStack
VpcPeersStack: deploying...
✅ VpcPeersStack (no changes)
Outputs:
VpcPeersStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.0.0/24
VpcPeersStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0dd8a9cd265dc8acb
VpcPeersStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.1.0/24
VpcPeersStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0496c16092cdd8311
VpcPeersStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-07277da5218b90290
VpcPeersStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-005f39777bccd74f4
VpcPeersStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-0a018df57060948a4
VpcPeersStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0c5433d68b3f2f67c
VpcPeersStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-02ca74736f4f0ea17
VpcPeersStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-048b1e861592d392c
Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcPeersStack/d91b71b0-2dbf-11eb-8c69-06b222f0b0a4
InstancePeersStack
InstancePeersStack: deploying...
InstancePeersStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (10/10)
✅ InstancePeersStack
Outputs:
InstancePeersStack.Instance0BastionHostId1959CA92 = i-0ca24549d1646cccd # <--- COPY THE ID OF YOUR SOURCE EC2 INSTANCE!
InstancePeersStack.Instance0PrivateIp = 10.0.0.36
InstancePeersStack.Instance1BastionHostIdEF2AA144 = i-0fec2bdd51392974d
InstancePeersStack.Instance1PrivateIp = 10.0.1.59 # <--- COPY THE PRIVATE IP OF YOUR DESTINATION EC2 INSTANCE!
Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/InstancePeersStack/9e40f500-2dc0-11eb-aab0-0a253e5a178e
PeeringStack
PeeringStack: deploying...
PeeringStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (5/5)
✅ PeeringStack
Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/PeeringStack/15e96f10-2dc1-11eb-ae91-0643678755c5
Validation
To test if the VPC Peering has been properly set up, we're gonna send 3 pings from one of the EC2 instances to the other using the AWS CLI and its aws ssm send-command
command.
If you're following along, be sure to swap the ID of the source EC2 instance (i-0ca24549d1646cccd
) and the private IP of the destination EC2 instance (10.0.1.59
) for appropriate values before running the below:
➜ ping-me-cdk-example$ aws ssm send-command \
--document-name "AWS-RunShellScript" \
--document-version "1" \
--targets '[{"Key":"InstanceIds","Values":["i-0ca24549d1646cccd"]}]' \
--parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["ping 10.0.1.59 -c 3"]}' \
--timeout-seconds 600 \
--max-concurrency "50" \
--max-errors "0"
{
"Command": {
"CommandId": "e2171883-d9d1-478c-9ad2-2c7c51ca6c2e",
"DocumentName": "AWS-RunShellScript",
"DocumentVersion": "1",
"Comment": "",
"ExpiresAfter": "2020-11-23T22:04:17.410000+01:00",
"Parameters": {
"commands": [
"ping 10.0.1.59 -c 3"
],
"executionTimeout": [
"3600"
],
"workingDirectory": [
""
]
},
"InstanceIds": [],
"Targets": [
{
"Key": "InstanceIds",
"Values": [
"i-0ca24549d1646cccd"
]
}
],
"RequestedDateTime": "2020-11-23T20:54:17.410000+01:00",
"Status": "Pending",
"StatusDetails": "Pending",
"OutputS3BucketName": "",
"OutputS3KeyPrefix": "",
"MaxConcurrency": "50",
"MaxErrors": "0",
"TargetCount": 0,
"CompletedCount": 0,
"ErrorCount": 0,
"DeliveryTimedOutCount": 0,
"ServiceRole": "",
"NotificationConfig": {
"NotificationArn": "",
"NotificationEvents": [],
"NotificationType": ""
},
"CloudWatchOutputConfig": {
"CloudWatchLogGroupName": "",
"CloudWatchOutputEnabled": false
},
"TimeoutSeconds": 600
}
}
Now, let's check whether that succeeded using AWS CLI's aws ssm get-command-invocation
command.
Again, if you're following along, be sure to swap the command ID (e2171883-d9d1-478c-9ad2-2c7c51ca6c2e
) and the ID of the source EC2 instance (i-0ca24549d1646cccd
) for appropriate values before running the below:
➜ ping-me-cdk-example$ aws ssm get-command-invocation --command-id e2171883-d9d1-478c-9ad2-2c7c51ca6c2e --instance-id i-0ca24549d1646cccd
{
"CommandId": "e2171883-d9d1-478c-9ad2-2c7c51ca6c2e",
"InstanceId": "i-0ca24549d1646cccd",
"Comment": "",
"DocumentName": "AWS-RunShellScript",
"DocumentVersion": "1",
"PluginName": "aws:runShellScript",
"ResponseCode": 0,
"ExecutionStartDateTime": "2020-11-23T19:54:17.876Z",
"ExecutionElapsedTime": "PT2.032S",
"ExecutionEndDateTime": "2020-11-23T19:54:19.876Z",
"Status": "Success",
"StatusDetails": "Success",
"StandardOutputContent": "PING 10.0.1.59 (10.0.1.59) 56(84) bytes of data.\n64 bytes from 10.0.1.59: icmp_seq=1 ttl=255 time=0.140 ms\n64 bytes from 10.0.1.59: icmp_seq=2 ttl=255 time=0.152 ms\n64 bytes from 10.0.1.59: icmp_seq=3 ttl=255 time=0.138 ms\n\n--- 10.0.1.59 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 2025ms\nrtt min/avg/max/mdev = 0.138/0.143/0.152/0.011 ms\n",
"StandardOutputUrl": "",
"StandardErrorContent": "",
"StandardErrorUrl": "",
"CloudWatchOutputConfig": {
"CloudWatchLogGroupName": "",
"CloudWatchOutputEnabled": false
}
}
3 packets transmitted, 3 received, 0% packet loss
, woop woop!
Cleanup
For the sake of our wallets, let's promptly destroy the current infrastructure before moving on. When prompted, type y
for yes:
➜ ping-me-cdk-example$ cdk destroy --all
Are you sure you want to delete: PeeringStack, InstancePeersStack, VpcPeersStack (y/n)? y
PeeringStack: destroying...
21:15:12 | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | PeeringStack
✅ PeeringStack: destroyed
InstancePeersStack: destroying...
21:16:03 | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | InstancePeersStack
✅ InstancePeersStack: destroyed
VpcPeersStack: destroying...
21:17:01 | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | VpcPeersStack
21:18:55 | DELETE_IN_PROGRESS | AWS::EC2::InternetGateway | Vpc1/IGW
21:18:55 | DELETE_IN_PROGRESS | AWS::EC2::VPC | Vpc1
✅ VpcPeersStack: destroyed
Okey doke, pinging over Site-to-Site VPN is next!
Posted on July 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.