Ping Me! (Part 1: VPC Peering Using CDK)

Overview

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/[email protected]
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN @aws-cdk/[email protected] requires a peer of @aws-cdk/[email protected] but none is installed. You must install peer dependencies yourself.
npm WARN [email protected] No repository field.
npm WARN [email protected] No license field.

+ @aws-cdk/[email protected]
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)
  },
});
➜  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

26