06.05, Katowice AWS Summit Poland
16 min readPart 4/4

Connecting Amazon VPCs via AWS Transit Gateway

A practical example of pinging Amazon VPCs connected via AWS Transit Gateway, built and deployed using the AWS Cloud Development Kit.



Transit Gateway (TGW) is a relatively new thing on AWS, but one that has greatly simplified networking, especially for more complex topologies (e.g. dozens or even hundreds of VPCs spanned across different AWS regions and accounts).

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 Transit Gateway.

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.

Cost-wise, you pay for two things: the number of attachments to a TGW (there’s an hourly rate) and data transfer.

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.

A rudimentary diagram of the complete solution

Once again, we’ll reuse the VpcStack and InstanceStack classes that we created in part 1. Additionally, we’ll create two classes: one for the Transit Gateway itself, and the other for routes leading to it:

ping-me-cdk-example/lib/tgw.ts 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 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 VPCsconst 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 VPCnew InstanceStack(app, 'InstanceTransitStack', {  vpcs: vpcsMetInTransit.createdVpcs,});// Create a Transit Gateway and attach both VPCs to itnew TransitGatewayStack(app, 'TransitGatewayStack', {  vpcs: [vpcsMetInTransit.createdVpcs[0], vpcsMetInTransit.createdVpcs[1]],});// Create routes between both VPCs over the Transit Gatewaynew 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 attempt to ping one from the other)

  ping-me-cdk-example$ cdk deploy InstanceTransitStack --require-approval neverIncluding dependency stacks: VpcsMetInTransitStackVpcsMetInTransitStackVpcsMetInTransitStack: deploying...VpcsMetInTransitStack: creating CloudFormation changeset...[██████████████████████████████████████████████████████████] (30/30)   VpcsMetInTransitStackOutputs:VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627dVpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15daVpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61cVpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680Stack ARN:arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5InstanceTransitStackInstanceTransitStack: deploying...InstanceTransitStack: creating CloudFormation changeset...[██████████████████████████████████████████████████████████] (10/10)   InstanceTransitStackOutputs:InstanceTransitStack.Instance0BastionHostId1959CA92 = i-03d7c391c35302d4a # <- COPY THE ID OF YOUR SOURCE EC2 INSTANCE!InstanceTransitStack.Instance0PrivateIp = 10.0.4.58InstanceTransitStack.Instance1BastionHostIdEF2AA144 = i-0d315dbb89ed80f82InstanceTransitStack.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 neverIncluding dependency stacks: VpcsMetInTransitStackVpcsMetInTransitStackVpcsMetInTransitStack: deploying...   VpcsMetInTransitStack (no changes)Outputs:VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627dVpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15daVpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61cVpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680Stack ARN:arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5TransitGatewayStackTransitGatewayStack: deploying...TransitGatewayStack: creating CloudFormation changeset...[██████████████████████████████████████████████████████████] (5/5)   TransitGatewayStackOutputs:TransitGatewayStack.TransitGatewayId = tgw-057de86d7c789626eStack 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 neverIncluding dependency stacks: VpcsMetInTransitStackVpcsMetInTransitStackVpcsMetInTransitStack: deploying...   VpcsMetInTransitStack (no changes)Outputs:VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30CidrBlockB8164F9E = 10.0.4.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc07C831B30DefaultSecurityGroup52C351BF = sg-0b97deeeacfd5627dVpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BCidrBlock933A5AA8 = 10.0.5.0/24VpcsMetInTransitStack.ExportsOutputFnGetAttVpc1C211860BDefaultSecurityGroup87C47BC2 = sg-0dfe3c25a0cd42e84VpcsMetInTransitStack.ExportsOutputRefVpc07C831B304FE08623 = vpc-063e0aaa8aaf32b32VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1RouteTableB5C6777D52F53FE8 = rtb-053b342d2f9950c58VpcsMetInTransitStack.ExportsOutputRefVpc0privateSubnet1SubnetD6383522ACB05B9B = subnet-002286581738a15daVpcsMetInTransitStack.ExportsOutputRefVpc1C211860B64169B74 = vpc-0020c0197873df61cVpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1RouteTable339A93B3DFC75FCA = rtb-0b3cac3abd02c16d6VpcsMetInTransitStack.ExportsOutputRefVpc1privateSubnet1Subnet41967AFDFF883DAB = subnet-0c10da57ee874b680Stack ARN:arn:aws:cloudformation:eu-west-1:REDACTED:stack/VpcsMetInTransitStack/a54b7350-3560-11eb-ae91-0643678755c5RoutesToTransitGatewayStackRoutesToTransitGatewayStack: deploying...RoutesToTransitGatewayStack: creating CloudFormation changeset...[██████████████████████████████████████████████████████████] (4/4)   RoutesToTransitGatewayStackStack 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 construction process, the destruction 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 RoutesToTransitGatewayStackAre you sure you want to delete: RoutesToTransitGatewayStack (y/n)? yRoutesToTransitGatewayStack: destroying...14:08:49 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | RoutesToTransitGatewayStack14: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 --allAre you sure you want to delete: InstanceVpnDestinationStack, VpcVpnDestinationStack, TransitGatewayStack, RoutesToTransitGatewayStack, PeeringStack, InstanceTransitStack, InstancePeersStack, CustomerGatewayDeviceStack, VpcsMetInTransitStack, VpcVpnSourceStack, VpcPeersStack (y/n)? yInstanceVpnDestinationStack: destroying...   InstanceVpnDestinationStack: destroyedVpcVpnDestinationStack: destroying...   VpcVpnDestinationStack: destroyedTransitGatewayStack: destroying...14:12:23 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack         | TransitGatewayStack   TransitGatewayStack: destroyedRoutesToTransitGatewayStack: destroying...   RoutesToTransitGatewayStack: destroyedPeeringStack: destroying...   PeeringStack: destroyedInstanceTransitStack: destroying...14:15:30 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | InstanceTransitStack   InstanceTransitStack: destroyedInstancePeersStack: destroying...   InstancePeersStack: destroyedCustomerGatewayDeviceStack: destroying...   CustomerGatewayDeviceStack: destroyedVpcsMetInTransitStack: destroying...14:16:49 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack            | VpcsMetInTransitStack14:18:56 | DELETE_IN_PROGRESS   | AWS::EC2::InternetGateway             | Vpc0/IGW14:18:56 | DELETE_IN_PROGRESS   | AWS::EC2::VPC                         | Vpc0   VpcsMetInTransitStack: destroyedVpcVpnSourceStack: destroying...   VpcVpnSourceStack: destroyedVpcPeersStack: destroying...   VpcPeersStack: destroyed

Conclusion

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

Please remember that all the code is available on GitHub.

“That’s all Folks!” Hope you enjoyed this series — and until next time!

Let's talk about your project

We'd love to answer your questions and help you thrive in the cloud.