Exam Room · Advanced Architecture

Lab: Route Between VPCs With a Transit Gateway

November 20, 2026 · 11 min read

Advanced Cloud Architecture · part of The Exam Room

This is the second lab in the SA Pro hands-on track. The reference posts argue the decisions; this stands one up. It pairs with the peering-versus-Transit-Gateway post, which weighs the two approaches for connecting VPCs. Here we build the hub and do the wiring by hand, because the wiring is what there is to learn. The full lab is in lab-sa-pro-02-transit-gateway.zip.

Before your first lab, do the one-time, once-per-account setup: run the zip’s preflight.sh to confirm your account is ready, then deploy the lab reaper, a standing backstop that auto-deletes any lab you forget to tear down after 24 hours. Every SA Pro lab tags its stack for the reaper on deploy.

The scenario

Two VPCs need to reach each other, and there are more on the way. VPC peering handles a single pair fine, but peerings do not transit, so each new VPC has to peer with every existing one. That is an N-squared mesh: ten VPCs is forty-five peerings, and every route table grows with the estate.

A Transit Gateway is the hub. Each VPC attaches once, the gateway routes between the spokes, and the eleventh VPC is one attachment rather than ten new peerings. The gateway in this lab is created with default association and default propagation disabled, so nothing routes on its own. That is the design that is what a Professional paper rewards on a Professional paper: explicit routing, where you choose which attachments share a route table and what each one learns, is how you build segmentation later.

What you’re given

CloudFormation builds the Transit Gateway (with the two defaults disabled), VPC A on 10.10.0.0/16 and VPC B on 10.20.0.0/16, each with a subnet and its own route table, a VPC attachment for each VPC, and a custom TGW route table. What is missing is the routing itself. The gap in src/template.yaml is a TODO block where six resources go: the two associations (AWS::EC2::TransitGatewayRouteTableAssociation), the two propagations (AWS::EC2::TransitGatewayRouteTablePropagation), and the two return routes (AWS::EC2::Route, one in each VPC’s route table). Each is only a few lines: the associations and propagations name an attachment and the custom route table; the routes name a VPC route table, the other VPC’s CIDR, and the gateway.

Deploying the stack as shipped succeeds, but the two VPCs cannot reach each other: nothing is associated or propagated to the custom route table, and neither VPC has a route pointing at the gateway.

Your task

Fill in the TODO block with those six resources, one set for VPC A and the mirror image for VPC B. Two decisions do the work, and it helps to keep them separate in your head. Association picks which route table an attachment consults when it sends traffic into the gateway. Propagation installs an attachment’s routes into a route table, so the gateway learns 10.10.0.0/16 from VPC A’s attachment and 10.20.0.0/16 from VPC B’s. Associate and propagate both attachments to the one custom route table and the gateway knows how to reach either VPC.

That teaches the gateway, but not the VPCs. Each VPC’s route table still needs a route sending the other CIDR to the gateway: 10.20.0.0/16 via the Transit Gateway in VPC A, 10.10.0.0/16 via the Transit Gateway in VPC B. The DependsOn on both attachments matters, because a route to a CIDR reachable through the gateway cannot be created until the gateway has something attached.

Run it

./scripts/deploy.sh          # deploys src/template.yaml
./scripts/test.sh            # checks the TGW and VPC route tables
./scripts/teardown.sh        # deletes everything

test.sh never launches an instance, which keeps it cheap. It reads the custom Transit Gateway route table with search-transit-gateway-routes and asserts both VPC CIDRs show up as active routes, then reads both VPC route tables and asserts each one has a route to the other VPC via a Transit Gateway. With the unedited template it reports nothing routing; with the wiring in place it prints:

  ok: TGW route table has an active route for 10.10.0.0/16
  ok: TGW route table has an active route for 10.20.0.0/16
  ok: rtb-... routes 10.20.0.0/16 via the Transit Gateway
  ok: rtb-... routes 10.10.0.0/16 via the Transit Gateway
PASS: both spokes are associated and propagated, and each VPC routes to the other via the Transit Gateway.

If it fails

  • No active routes in the TGW route table. Propagation is missing. The gateway only learns a VPC’s CIDR when that attachment is propagated to the route table; association alone does not install any routes.
  • The TGW route table has routes, but the VPCs still cannot reach each other. The return routes are missing. Propagation teaches the gateway; each VPC still needs its own route sending the other CIDR to the gateway.
  • A Route resource fails to create. Check the DependsOn. The route cannot be built until the attachments exist, so both attachments have to be named.

Reveal the solution

Work the six resources out yourself first; the wiring is the whole lab. When you want the reference answer, deploy it without editing anything:

SRC=solution ./scripts/deploy.sh && ./scripts/test.sh

Or unfold it here:

Show the answer

One set for VPC A; VPC B is the mirror image (swap the attachment, the route table, and the CIDR).

AssociationA:
  Type: AWS::EC2::TransitGatewayRouteTableAssociation
  Properties:
    TransitGatewayAttachmentId: !Ref AttachmentA
    TransitGatewayRouteTableId: !Ref TransitGatewayRouteTable

PropagationA:
  Type: AWS::EC2::TransitGatewayRouteTablePropagation
  Properties:
    TransitGatewayAttachmentId: !Ref AttachmentA
    TransitGatewayRouteTableId: !Ref TransitGatewayRouteTable

RouteAToB:
  Type: AWS::EC2::Route
  DependsOn: [AttachmentA, AttachmentB]
  Properties:
    RouteTableId: !Ref RouteTableA
    DestinationCidrBlock: 10.20.0.0/16
    TransitGatewayId: !Ref TransitGateway

What you just learned

  • A Transit Gateway replaces an N-squared mesh of peerings with a hub. Each VPC attaches once, and adding a spoke is one attachment rather than one peering per existing VPC.
  • Association and propagation are two separate decisions. Association picks the route table an attachment uses for outbound traffic; propagation installs an attachment’s routes into a route table so other attachments can reach it.
  • Disabling default association and propagation forces both decisions to be explicit, which is exactly how you build segmentation: attachments on different route tables cannot reach each other unless you propagate across.
  • Teaching the gateway is only half the job. Each VPC still needs a route in its own table pointing the other CIDR at the gateway, or the traffic never leaves the VPC.

Next

The rest of the SA Pro lab arc is in the track’s README: guarded cross-region backups, weighted blue/green traffic shifting, tag-driven cost visibility, and a DMS change-data-capture migration.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.