# AWS VPC Peering and Transit Gateway with Terraform

> Connect AWS VPCs with Terraform or OpenTofu using VPC peering for simple cases or a Transit Gateway for many networks, with routes and a comparison of costs.

- Source: https://www.itwonderlab.com/terraform-aws-vpc-peering-transit-gateway/
- Published: 2026-09-20
- Updated: 2026-09-20
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## Connecting VPCs

When you have more than one [VPC](https://www.itwonderlab.com/aws-vpc/) (one per environment, per team or per account), you need a way for them to talk using private IP addresses. The two main options are **VPC peering** and **AWS Transit Gateway**. The CIDR blocks of the connected VPCs **must not overlap**, so plan them in advance ([VPC tutorial](https://www.itwonderlab.com/aws-terraform-tutorial-aws-vpc/)).

### Comparison

| | VPC peering | Transit Gateway |
|---|---|---|
| Topology | One connection between two VPCs | A hub that connects many VPCs, VPNs and Direct Connect |
| Transitive routing | No (A-B and B-C does not mean A-C) | Yes |
| Number of connections | N x (N-1) / 2 for a full mesh | One attachment per VPC |
| Cost | No hourly charge, only data transfer between AZs or regions | Hourly charge per attachment plus data processed |
| Best for | Two or three VPCs | Many VPCs or hybrid networks |

### VPC peering in the same account and region

```hcl title="peering.tf"
resource "aws_vpc_peering_connection" "app_to_shared" {
  vpc_id      = aws_vpc.app.id
  peer_vpc_id = aws_vpc.shared.id
  auto_accept = true

  tags = {
    Name = "app-to-shared"
  }
}
```

`auto_accept` only works when both VPCs are in the same account and region. After the peering exists, **add routes on both sides**: the connection does nothing without them.

```hcl title="peering.tf"
resource "aws_route" "app_to_shared" {
  route_table_id            = aws_route_table.app_private.id
  destination_cidr_block    = aws_vpc.shared.cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.app_to_shared.id
}

resource "aws_route" "shared_to_app" {
  route_table_id            = aws_route_table.shared_private.id
  destination_cidr_block    = aws_vpc.app.cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.app_to_shared.id
}
```

Then allow the traffic in the [security groups](https://www.itwonderlab.com/aws-terraform-tutorial-aws-security-groups/) (reference the CIDR of the other VPC). Routing is explained in [routing tables](https://www.itwonderlab.com/aws-terraform-tutorial-aws-routing-tables/).

### Peering across accounts

The requester creates the connection and the owner of the other VPC accepts it, using a second provider with credentials for that account:

```hcl title="peering-cross-account.tf"
provider "aws" {
  alias   = "peer"
  region  = "eu-west-1"
  profile = "other-account"
}

data "aws_caller_identity" "peer" {
  provider = aws.peer
}

resource "aws_vpc_peering_connection" "cross" {
  vpc_id        = aws_vpc.app.id
  peer_vpc_id   = var.peer_vpc_id
  peer_owner_id = data.aws_caller_identity.peer.account_id
}

resource "aws_vpc_peering_connection_accepter" "cross" {
  provider                  = aws.peer
  vpc_peering_connection_id = aws_vpc_peering_connection.cross.id
  auto_accept               = true
}
```

### Transit Gateway

```hcl title="tgw.tf"
resource "aws_ec2_transit_gateway" "main" {
  description                     = "Central hub"
  default_route_table_association = "enable"
  default_route_table_propagation = "enable"
  dns_support                     = "enable"

  tags = {
    Name = "ditwl-tgw"
  }
}

resource "aws_ec2_transit_gateway_vpc_attachment" "vpc" {
  for_each = {
    app    = { vpc_id = aws_vpc.app.id,    subnet_ids = aws_subnet.app_tgw[*].id }
    shared = { vpc_id = aws_vpc.shared.id, subnet_ids = aws_subnet.shared_tgw[*].id }
  }

  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = each.value.vpc_id
  subnet_ids         = each.value.subnet_ids
}
```

With default association and propagation, every attached VPC can reach every other VPC. The routes inside the VPCs still need to point to the gateway:

```hcl title="tgw.tf"
resource "aws_route" "app_to_tgw" {
  route_table_id         = aws_route_table.app_private.id
  destination_cidr_block = "10.0.0.0/8"   # all your internal networks
  transit_gateway_id     = aws_ec2_transit_gateway.main.id

  depends_on = [aws_ec2_transit_gateway_vpc_attachment.vpc]
}
```

For isolation (for example production cannot reach development), disable the defaults and create separate Transit Gateway route tables and associations.

### Share it across accounts

Use AWS Resource Access Manager (`aws_ram_resource_share` and `aws_ram_principal_association`) to share the Transit Gateway with other accounts in your organization ([multi-account](https://www.itwonderlab.com/terraform-aws-organizations-multi-account/)), then each account creates its attachment.

### Costs

Transit Gateway charges per attachment per hour and per GB processed, so a small setup with two VPCs is much cheaper with peering. Peering has no hourly fee. Data transfer between AZs and regions applies to both. Consider [VPC endpoints](https://www.itwonderlab.com/terraform-aws-vpc-endpoints/) for access to specific services and estimate with [Infracost](https://www.itwonderlab.com/terraform-cost-estimation-infracost/).
