Skip to content

Transit Gateway Peering Attachment

Define a Transit Gateway Peering Attachment

Peer a local transit gateway to an existing transit gateway owned by another account/region (for example, a core network account's TGW deployed by AWS LZA). This repo only creates the requester side of the peering attachment. The peer's owner must accept the peering request (and manage any route tables on their own TGW) themselves - that is out of scope here.

tgws = {
    tgw = {
        peering_attachments = {
            CoreNetwork = {
                peer_transit_gateway_id = "tgw-0123456789abcdef0"
                peer_account_id = "111122223333"
                peer_region = "us-east-1"
                route_table = "Spoke"
            }
        }
    }
}
route_table associates the peering attachment to a local TGW route table (defined under route_tables), which determines how traffic entering the local TGW from the peer gets routed onward.

Peering attachment keys share the same key namespace as VPC attachment keys on a given tgw (both end up referenced as transit_gateway_attachment = "<key>" in routes). Don't reuse a key between attachments and peering_attachments on the same tgw - a collision resolves silently in favor of the peering attachment for routes (since the two are merged together before being passed to the route module), but fails outright when creating the route table association for that key.

Define Static Routes Across the Peering Attachment

Transit Gateway peering attachments only support static routes - dynamic route propagation is not supported across a peering attachment (unlike VPC or VPN attachments). Add a route for each remote CIDR you need to reach, specifying the peering attachment's key as the transit_gateway_attachment.

tgws = {
    tgw = {
        route_tables = {
            Spoke = {
                routes = [
                    {
                        destination_cidr_block = "10.240.0.0/16"
                        transit_gateway_attachment = "CoreNetwork"
                    }
                ]
            }
        }
        peering_attachments = {
            CoreNetwork = {
                peer_transit_gateway_id = "tgw-0123456789abcdef0"
                peer_account_id = "111122223333"
                peer_region = "us-east-1"
                route_table = "Spoke"
            }
        }
    }
}

Apply Order: This Is a Two-Phase Rollout

A peering attachment sits in pendingAcceptance until the peer's owner accepts it (see the note on acceptance being out of scope above). AWS will not let you associate a route table or add a static route to an attachment that isn't yet available, so the route and route table association resources will always fail if applied in the same run as a peering attachment that hasn't been accepted yet - this is expected, not a bug:

Error: creating EC2 Transit Gateway Route (...): api error IncorrectState: tgw-attach-xxxxxxxx is in invalid state
Error: creating EC2 Transit Gateway Route Table Association (...): api error IncorrectState: tgw-attach-xxxxxxxx is in invalid state
Since deployments here go through a pipeline that only runs plan/apply (no -target, no direct terraform access), this rollout is done by commenting/uncommenting the relevant tfvars blocks across two separate pipeline runs rather than targeting a specific resource: 1. Phase 1: uncomment only the peering_attachments block, leave the routes that reference it commented out, and deploy. This creates just the peering attachment (requester side), which will sit in pendingAcceptance. 2. Have the peer's owner accept the request in their account/region (see the acceptance note above for where in the console). 3. Phase 2: uncomment the static routes that reference the peering attachment (and the route table association follows automatically via the attachment's route_table field) and deploy again - since the attachment is now available, these will succeed this time.

See the commented-out example in environments/nonprod/gianni_ire.tfvars for how this phasing is annotated inline. Any time peer_transit_gateway_id/peer_account_id/peer_region change, the peering attachment is replaced and this same two-phase sequence repeats.

Cleaning Up a Failed Attachment

If a peering attachment request fails outright (for example, an incorrect peer_transit_gateway_id/peer_account_id/peer_region combination - check the reason with aws ec2 describe-transit-gateway-peering-attachments --transit-gateway-attachment-ids <id>), the AWS provider treats the remote object as gone on the next refresh and drops it from state rather than issuing a delete - so a subsequent apply just creates a new attachment instead of cleaning up the old one. The failed attachment is left behind as an orphan in the requester's account and needs to be deleted manually:

aws ec2 delete-transit-gateway-peering-attachment --transit-gateway-attachment-id <failed-attachment-id> --region <requester-region>