Ping Me! (Part 3: Transit Gateway Using CDK)

Overview

In short, it's a powerful beast that acts as a highly scalable cloud router. A single TGW can support up to 5,000 attachments, where an attachment can be a VPC, a Direct Connect Gateway (DXGW), a VPN connection or a peering connection to another TGW.

Traffic between a TGW and a VPC, as well as any inter-region traffic, stays on the AWS backbone network.

There is a multitude of scenarios for using a TGW (mesh networks, hub-and-spoke networks, isolated VPCs with shared services, etc.) and it'd be virtually impossible to build an enterprise-grade infrastructure on AWS without using one.

Implementation

As is the common theme in this series, we'll connect two VPCs together to make a successful ping between EC2 instances placed in both of them. This time a Transit Gateway (TGW) is going to be the glue.

Once again, we'll reuse the VpcStack and InstanceStack classes that we created in part 1. Additionally, we'll create two classes, both will live in one file, one for a TGW and the other for routes leading to it:

➜  ping-me-cdk-example$ touch lib/tgw.ts
  • ping-me-cdk-example/lib/tgw.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as cdk from '@aws-cdk/core';

interface TransitGatewayProps extends cdk.StackProps {
    vpcs: [ec2.Vpc, ec2.Vpc, ...ec2.Vpc[]]; // <--- a list of VPC objects (at least two are required) to be attached to the Transit Gateway; NB only routes between the first two VPCs will be created
}

export class TransitGatewayStack extends cdk.Stack {

    constructor(scope: cdk.Construct, id: string, props: TransitGatewayProps) {
        super(scope, id, props);

        // create a Transit Gateway
        const tgw = new ec2.CfnTransitGateway(this, 'Tgw');

        // For each supplied VPC, create a Transit Gateway attachment
        props.vpcs.forEach((vpc, index) => {
            new ec2.CfnTransitGatewayAttachment(this, `TgwVpcAttachment${index}`, {
                subnetIds: vpc.privateSubnets.map(privateSubnet => privateSubnet.subnetId),
                transitGatewayId: tgw.ref,
                vpcId: vpc.vpcId,
            });
        });

        // Output the Transit Gateway's ID
        new cdk.CfnOutput(this, 'TransitGatewayId', {
            value: tgw.ref,
            exportName: 'TransitGatewayId',
        });
    }
}

export class RoutesToTransitGatewayStack extends cdk.Stack {

    constructor(scope: cdk.Construct, id: string, props: TransitGatewayProps) {
        super(scope, id, props);

        // Add route from the private subnet of the first VPC to the second VPC over the Transit Gateway
        // 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,
                transitGatewayId: cdk.Fn.importValue('TransitGatewayId'), // Transit Gateway must already exist
            });
        });

        // Add route from the private subnet of the second VPC to the first VPC over the Transit Gateway
        props.vpcs[1].privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
            new ec2.CfnRoute(this, 'RouteFromPrivateSubnetOfVpc2ToVpc1' + index, {
                destinationCidrBlock: props.vpcs[0].vpcCidrBlock,
                routeTableId,
                transitGatewayId: cdk.Fn.importValue('TransitGatewayId'), // Transit Gateway must already exist
            });
        });
    }
}

With these four classes at our disposal, we can initialize the necessary stacks:

  • 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';
import { CustomerGatewayDeviceStack } from '../lib/cgd';
import { TransitGatewayStack, RoutesToTransitGatewayStack } from '../lib/tgw';

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

/**
 * CODE FROM "Ping Me! (Part 1: VPC Peering Using CDK)" AND "Ping Me! (Part 2: Site-to-Site VPN Using CDK)" WAS REMOVED FOR VISIBILITY
 */

 // Create two VPCs
const vpcsMetInTransit = new VpcStack(app, 'VpcsMetInTransitStack', {
  vpcSetup: {
    cidrs: ['10.0.4.0/24', '10.0.5.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, 'InstanceTransitStack', {
  vpcs: vpcsMetInTransit.createdVpcs,
});

// Create a Transit Gateway and attach both VPCs to it
new TransitGatewayStack(app, 'TransitGatewayStack', {
  vpcs: [vpcsMetInTransit.createdVpcs[0], vpcsMetInTransit.createdVpcs[1]],
});

// Create routes between both VPCs over the Transit Gateway
new RoutesToTransitGatewayStack(app, 'RoutesToTransitGatewayStack', {
  vpcs: [vpcsMetInTransit.createdVpcs[0], vpcsMetInTransit.createdVpcs[1]],
});

The deployment will be done in three stages:

  • first, InstanceTransitStack (implicitly with VpcsMetInTransitStack).

(During this step you can grab the ID of your source EC2 instance and the private IP of your destination EC2 instance.
Both will come in handy in a bit when we'll attempt to ping one from the other.)

➜  ping-me-cdk-example$ cdk deploy InstanceTransitStack --require-approval never
Including dependency stacks: VpcsMetInTransitStack
VpcsMetInTransitStack
VpcsMetInTransitStack: deploying...
VpcsMetInTransitStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (30/30)

 ✅  VpcsMetInTransitStack

Outputs:
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627d
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84
VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15da
VpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61c
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5
InstanceTransitStack
InstanceTransitStack: deploying...
InstanceTransitStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (10/10)

 ✅  InstanceTransitStack

Outputs:
InstanceTransitStack.Instance0BastionHostId1959CA92 = i-03d7c391c35302d4a # <--- COPY THE ID OF YOUR SOURCE EC2 INSTANCE!
InstanceTransitStack.Instance0PrivateIp = 10.0.4.58
InstanceTransitStack.Instance1BastionHostIdEF2AA144 = i-0d315dbb89ed80f82
InstanceTransitStack.Instance1PrivateIp = 10.0.5.54 # <--- COPY THE PRIVATE IP OF YOUR DESTINATION EC2 INSTANCE!

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/InstanceTransitStack/11f0b6a0-3561-11eb-842c-0aa13688a741
  • then TransitGatewayStack
➜  ping-me-cdk-example$ cdk deploy TransitGatewayStack --require-approval never
Including dependency stacks: VpcsMetInTransitStack
VpcsMetInTransitStack
VpcsMetInTransitStack: deploying...

 ✅  VpcsMetInTransitStack (no changes)

Outputs:
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627d
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84
VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15da
VpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61c
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5
TransitGatewayStack
TransitGatewayStack: deploying...
TransitGatewayStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (5/5)

 ✅  TransitGatewayStack

Outputs:
TransitGatewayStack.TransitGatewayId = tgw-057de86d7c789626e

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/TransitGatewayStack/e86b7b70-3561-11eb-b82a-0ad12ebbcfd9
  • and finally RoutesToTransitGatewayStack
➜  ping-me-cdk-example$ cdk deploy RoutesToTransitGatewayStack --require-approval never
Including dependency stacks: VpcsMetInTransitStack
VpcsMetInTransitStack
VpcsMetInTransitStack: deploying...

 ✅  VpcsMetInTransitStack (no changes)

Outputs:
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627d
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24
VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84
VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58
VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15da
VpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61c
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6
VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5
RoutesToTransitGatewayStack
RoutesToTransitGatewayStack: deploying...
RoutesToTransitGatewayStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (4/4)

 ✅  RoutesToTransitGatewayStack

Stack ARN:
arn:aws:cloudformation:eu-west-1:REDACTED:stack/RoutesToTransitGatewayStack/d803d8d0-3562-11eb-aaeb-02e586bc56f0

Validation

It's time to unleash the ping!

If you're following along, be sure to swap the ID of the source EC2 instance (i-03d7c391c35302d4a) and the private IP of the destination EC2 instance (10.0.5.54) for appropriate values before running the below:

aws ssm send-command \
--document-name "AWS-RunShellScript" \
--document-version "1" \
--targets '[{"Key":"InstanceIds","Values":["i-03d7c391c35302d4a"]}]' \
--parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["ping 10.0.5.54 -c 3"]}' \
--timeout-seconds 600 \
--max-concurrency "50" \
--max-errors "0"
{
    "Command": {
        "CommandId": "f7ed8e0e-a313-405a-a811-7885b4d532e7",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "1",
        "Comment": "",
        "ExpiresAfter": "2020-12-03T15:06:43.691000+01:00",
        "Parameters": {
            "commands": [
                "ping 10.0.5.54 -c 3"
            ],
            "executionTimeout": [
                "3600"
            ],
            "workingDirectory": [
                ""
            ]
        },
        "InstanceIds": [],
        "Targets": [
            {
                "Key": "InstanceIds",
                "Values": [
                    "i-03d7c391c35302d4a"
                ]
            }
        ],
        "RequestedDateTime": "2020-12-03T13:56:43.691000+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 by using AWS CLI's aws ssm get-command-invocation command.

Again, if you're following along, be sure to swap the command ID (f7ed8e0e-a313-405a-a811-7885b4d532e7) and the ID of the source EC2 instance (i-03d7c391c35302d4a) for appropriate values before running the below:

➜  ping-me-cdk-example$ aws ssm get-command-invocation --command-id f7ed8e0e-a313-405a-a811-7885b4d532e7 --instance-id i-03d7c391c35302d4a
{
    "CommandId": "f7ed8e0e-a313-405a-a811-7885b4d532e7",
    "InstanceId": "i-03d7c391c35302d4a",
    "Comment": "",
    "DocumentName": "AWS-RunShellScript",
    "DocumentVersion": "1",
    "PluginName": "aws:runShellScript",
    "ResponseCode": 0,
    "ExecutionStartDateTime": "2020-12-03T12:56:44.343Z",
    "ExecutionElapsedTime": "PT2.044S",
    "ExecutionEndDateTime": "2020-12-03T12:56:46.343Z",
    "Status": "Success",
    "StatusDetails": "Success",
    "StandardOutputContent": "PING 10.0.5.54 (10.0.5.54) 56(84) bytes of data.\n64 bytes from 10.0.5.54: icmp_seq=1 ttl=254 time=0.489 ms\n64 bytes from 10.0.5.54: icmp_seq=2 ttl=254 time=0.311 ms\n64 bytes from 10.0.5.54: icmp_seq=3 ttl=254 time=0.306 ms\n\n--- 10.0.5.54 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 2027ms\nrtt min/avg/max/mdev = 0.306/0.368/0.489/0.087 ms\n",
    "StandardOutputUrl": "",
    "StandardErrorContent": "",
    "StandardErrorUrl": "",
    "CloudWatchOutputConfig": {
        "CloudWatchLogGroupName": "",
        "CloudWatchOutputEnabled": false
    }
}

3 packets transmitted, 3 received, 0% packet loss. That's an astounding success!

Cleanup

For the sake of our wallets, let's promptly destroy the current infrastructure before wrapping everything up.

As was the case with the building process, the destroying part must also be done in stages:

  • first we need to remove the routes to the Transit Gateway. When prompted, type y for yes:
➜  ping-me-cdk-example$ cdk destroy RoutesToTransitGatewayStack
Are you sure you want to delete: RoutesToTransitGatewayStack (y/n)? y
RoutesToTransitGatewayStack: destroying...
14:08:49 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | RoutesToTransitGatewayStack
14:08:51 | DELETE_IN_PROGRESS   | AWS::EC2::Route    | RouteFromPrivateSubnetOfVpc2ToVpc10
 ✅  RoutesToTransitGatewayStack: destroyed
  • once the routes are removed we can safely delete the remaining stacks. When prompted, type y for yes:
➜  ping-me-cdk-example$ cdk destroy --all
Are you sure you want to delete: InstanceVpnDestinationStack, VpcVpnDestinationStack, TransitGatewayStack, RoutesToTransitGatewayStack, PeeringStack, InstanceTransitStack, InstancePeersStack, CustomerGatewayDeviceStack, VpcsMetInTransitStack, VpcVpnSourceStack, VpcPeersStack (y/n)? y
InstanceVpnDestinationStack: destroying...
 ✅  InstanceVpnDestinationStack: destroyed
VpcVpnDestinationStack: destroying...
 ✅  VpcVpnDestinationStack: destroyed
TransitGatewayStack: destroying...
14:12:23 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack         | TransitGatewayStack
 ✅  TransitGatewayStack: destroyed
RoutesToTransitGatewayStack: destroying...
 ✅  RoutesToTransitGatewayStack: destroyed
PeeringStack: destroying...
 ✅  PeeringStack: destroyed
InstanceTransitStack: destroying...
14:15:30 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | InstanceTransitStack
 ✅  InstanceTransitStack: destroyed
InstancePeersStack: destroying...
 ✅  InstancePeersStack: destroyed
CustomerGatewayDeviceStack: destroying...
 ✅  CustomerGatewayDeviceStack: destroyed
VpcsMetInTransitStack: destroying...
14:16:49 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack            | VpcsMetInTransitStack
14:18:56 | DELETE_IN_PROGRESS   | AWS::EC2::InternetGateway             | Vpc0/IGW
14:18:56 | DELETE_IN_PROGRESS   | AWS::EC2::VPC                         | Vpc0
 ✅  VpcsMetInTransitStack: destroyed
VpcVpnSourceStack: destroying...
 ✅  VpcVpnSourceStack: destroyed
VpcPeersStack: destroying...
 ✅  VpcPeersStack: destroyed

Conclusion

In this series of articles, we saw how with relative ease you can use Cloud Development Kit (CDK) to create, update and destroy various AWS resources, and further bind them all together in a configuration that best suits your needs.

In Ping Me! (Part 1: VPC Peering Using CDK) we wrote our first classes and then initialized them as stacks.

The final part centered around one of the coolest AWS network resources, namely the Transit Gateway (TGW).

"That's all Folks!" Hope you enjoyed the read and until next time!

29