VPC Peering Connection
Use Case: Your ECS tasks in Account A need to connect to an RDS database in Account B over a private network — no public internet involved.
🧩 The Problem
You have two AWS accounts with separate VPCs:
| Resource | Account | VPC CIDR (example) |
| ECS Cluster (Fargate/EC2) | Account A — 111111111111 | 10.0.0.0/16 |
| RDS Database (PostgreSQL/MySQL) | Account B — 222222222222 | 10.1.0.0/16 |
By default, these two VPCs are completely isolated. Your ECS tasks cannot resolve or reach the RDS endpoint in the other account. You need a VPC Peering Connection to create a private network bridge between them.
🔑 What Is VPC Peering?
A VPC peering connection is a one-to-one networking link between two VPCs that routes traffic using private IP addresses. Resources in either VPC communicate as if they were in the same network.
Key characteristics:
-
Works across accounts and across regions (inter-region peering)
-
Traffic stays on the AWS backbone — never traverses the public internet
-
Inter-region traffic is encrypted automatically
-
No single point of failure or bandwidth bottleneck
-
No charge to create the peering connection (standard data transfer rates apply)
-
Data transfer within the same Availability Zone is free, even across accounts
Critical constraint: The two VPCs must not have overlapping CIDR blocks. If 10.0.0.0/16 and 10.0.0.0/16 overlap, VPC peering will not work. You would need to use AWS PrivateLink or re-address one of the VPCs.
🏗️ Architecture Overview
┌──────────────────────────────────────────────────────────────────────────┐
│ VPC Peering Connection │
│ (pcx-0abc123...) │
│ │
│ Account A (111111111111) Account B (222222222222) │
│ VPC: 10.0.0.0/16 VPC: 10.1.0.0/16 │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Private Subnet(s) │ │ Private Subnet(s) │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ ECS Tasks │ │◄────────►│ │ RDS Instance │ │ │
│ │ │ (Fargate) │ │ Private │ │ (PostgreSQL) │ │ │
│ │ └───────────────┘ │ Traffic │ └───────────────┘ │ │
│ │ │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ Route Table: Route Table: │
│ 10.1.0.0/16 → pcx-0abc123 10.0.0.0/16 → pcx-0abc123 │
│ │
│ Security Group: Security Group: │
│ Outbound → 10.1.0.0/16:5432 Inbound ← 10.0.0.0/16:5432 │
└──────────────────────────────────────────────────────────────────────────┘
📋 Prerequisites Checklist
Before you begin, confirm the following:
-
You have admin or sufficient IAM permissions in both AWS accounts (VPC, EC2, RDS, IAM)
-
The CIDR blocks of the two VPCs do not overlap
-
You know the VPC ID in both accounts
-
You know the Account ID of the peer account
-
The RDS instance is in a private subnet (no public accessibility required)
-
Both VPCs have DNS hostnames and DNS resolution enabled
🚀 Step-by-Step Setup
Step 1 — Create the VPC Peering Connection (Account A — Requester)
-
Open the VPC Console → Peering Connections → Create peering connection
-
Fill in the details:
- Name:
ecs-to-rds-cross-account-peer - VPC ID (Requester): Select the VPC where your ECS cluster runs
- Account: Choose Another account → enter Account B's ID (
222222222222) - Region: Choose This Region (same region) or Another Region if cross-region
- VPC ID (Accepter): Enter the VPC ID from Account B where RDS is hosted
- Name:
-
Click Create peering connection The connection status will show as Pending Acceptance.
AWS CLI equivalent:
aws ec2 create-vpc-peering-connection \
--vpc-id vpc-0aaa1111aaaa1111a \
--peer-vpc-id vpc-0bbb2222bbbb2222b \
--peer-owner-id 222222222222 \
--peer-region us-east-1 \
--tag-specifications 'ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=ecs-to-rds-cross-account-peer}]'
Step 2 — Accept the Peering Connection (Account B — Accepter)
-
Log into Account B
-
Open the VPC Console → Peering Connections
-
Find the pending peering request from Account A
-
Select it → Actions → Accept request
-
Confirm the acceptance The status should change to Active.
AWS CLI equivalent:
aws ec2 accept-vpc-peering-connection \
--vpc-peering-connection-id pcx-0abc123def456ghi7
Step 3 — Update Route Tables (Both Accounts)
This is the step people most commonly miss. Both VPCs need routes pointing to the peer VPC's CIDR through the peering connection.
Account A — ECS VPC Route Table
Add a route to the subnets where your ECS tasks run:
| Destination | Target |
10.1.0.0/16 (Account B's VPC CIDR) | pcx-0abc123def456ghi7 (Peering Connection) |
Important: Update the route tables for every subnet where ECS tasks may launch. If you are using Fargate, update the route tables for the subnets specified in your ECS service's networkConfiguration.
aws ec2 create-route \
--route-table-id rtb-0aaa1111 \
--destination-cidr-block 10.1.0.0/16 \
--vpc-peering-connection-id pcx-0abc123def456ghi7
Account B — RDS VPC Route Table
Add a route to the subnets where RDS is deployed:
| Destination | Target |
10.0.0.0/16 (Account A's VPC CIDR) | pcx-0abc123def456ghi7 (Peering Connection) |
Update the route tables associated with all subnets in the RDS subnet group.
aws ec2 create-route \
--route-table-id rtb-0bbb2222 \
--destination-cidr-block 10.0.0.0/16 \
--vpc-peering-connection-id pcx-0abc123def456ghi7
Step 4 — Configure Security Groups
Account B — RDS Security Group (Inbound)
Allow traffic from Account A's ECS VPC on the database port:
| Type | Protocol | Port | Source | Description |
| Custom TCP | TCP | 5432 (PostgreSQL) or 3306 (MySQL) | 10.0.0.0/16 | Allow ECS from Account A |
Note: For same-region, same-account peering you can reference security groups directly. For cross-account peering, you must use the CIDR block as the source — security group references across accounts are only supported for peering connections within the same region.
aws ec2 authorize-security-group-ingress \
--group-id sg-0bbb2222rds \
--protocol tcp \
--port 5432 \
--cidr 10.0.0.0/16
Account A — ECS Task Security Group (Outbound)
If your ECS task security group restricts outbound traffic (not the default 0.0.0.0/0 allow-all), add an outbound rule:
| Type | Protocol | Port | Destination | Description |
| Custom TCP | TCP | 5432 | 10.1.0.0/16 | Allow outbound to RDS in Account B |
Step 5 — Enable DNS Resolution for VPC Peering
This is a critical step that is easy to overlook. By default, when your ECS task resolves the RDS endpoint hostname (e.g., mydb.abc123.us-east-1.rds.amazonaws.com), it may resolve to the public IP instead of the private IP. Enabling DNS resolution on the peering connection ensures the RDS endpoint resolves to its private IP from the peer VPC.
-
In Account A (requester): Go to VPC Console → Peering Connections → select the connection → Actions → Edit DNS settings
- Enable: Requester DNS resolution — "Allow accepter VPC to resolve DNS of requester VPC hosts to private IP"
-
In Account B (accepter): Same steps but enable Accepter DNS resolution — "Allow requester VPC to resolve DNS of accepter VPC hosts to private IP" Both sides must enable their respective setting independently.
AWS CLI:
# Run from Account A
aws ec2 modify-vpc-peering-connection-options \
--vpc-peering-connection-id pcx-0abc123def456ghi7 \
--requester-peering-connection-options AllowDnsResolutionFromRemoteVpc=true
# Run from Account B
aws ec2 modify-vpc-peering-connection-options \
--vpc-peering-connection-id pcx-0abc123def456ghi7 \
--accepter-peering-connection-options AllowDnsResolutionFromRemoteVpc=true
Also ensure both VPCs have these settings enabled:
-
Enable DNS hostnames →
true -
Enable DNS resolution →
true
Step 6 — Verify Network ACLs
Network ACLs (NACLs) are stateless and evaluated before security groups. Make sure the NACLs on both subnets allow:
Account A subnets (ECS):
-
Outbound: Allow TCP to
10.1.0.0/16on port5432 -
Inbound: Allow TCP from
10.1.0.0/16on ephemeral ports (1024-65535) — for return traffic
Account B subnets (RDS):
-
Inbound: Allow TCP from
10.0.0.0/16on port5432 -
Outbound: Allow TCP to
10.0.0.0/16on ephemeral ports (1024-65535) — for return traffic If you are using the default NACLs (allow all), no changes are needed.
Step 7 — Configure ECS Task to Connect
In your ECS task definition, pass the RDS endpoint as an environment variable or use AWS Secrets Manager for credentials.
Task definition snippet (Fargate, awsvpc mode):
{
"containerDefinitions": [
{
"name": "my-app",
"image": "my-app:latest",
"environment": [
{
"name": "DB_HOST",
"value": "mydb.abc123.us-east-1.rds.amazonaws.com"
},
{
"name": "DB_PORT",
"value": "5432"
}
],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:222222222222:secret:mydb-creds"
}
]
}
],
"networkMode": "awsvpc"
}
For cross-account Secrets Manager access: The secret in Account B must have a resource policy allowing Account A's ECS task execution role to read it. Alternatively, store a copy of the credentials in Account A's Secrets Manager.
✅ Verification & Testing
Test from within an ECS task or EC2 in Account A
# DNS resolution check — should return a private IP in 10.1.x.x range
dig mydb.abc123.us-east-1.rds.amazonaws.com +short
# Connectivity check
telnet mydb.abc123.us-east-1.rds.amazonaws.com 5432
# Or use nc (netcat)
nc -zv mydb.abc123.us-east-1.rds.amazonaws.com 5432
What to check if it fails
| Symptom | Likely Cause | Fix |
| DNS resolves to public IP | DNS resolution not enabled on peering connection | Enable requester/accepter DNS resolution (Step 5) |
| Connection times out | Missing route in route table | Add route to peer CIDR via peering connection (Step 3) |
| Connection refused | Security group blocking | Add inbound rule on RDS SG for Account A CIDR (Step 4) |
| Peering shows "Pending" | Not accepted yet | Accept in Account B (Step 2) |
| Peering shows "Failed" | Overlapping CIDRs | Re-address one VPC or use PrivateLink instead |
| Intermittent failures | Route table only updated on some subnets | Update ALL subnets where ECS tasks or RDS instances reside |
| Works from EC2 but not ECS | Wrong security group on ECS task | In awsvpc mode, the task security group governs access, not the instance SG |
🔄 Terraform Example
# ─── Account A (ECS) ───
resource "aws_vpc_peering_connection" "ecs_to_rds" {
vpc_id = aws_vpc.ecs_vpc.id
peer_vpc_id = "vpc-0bbb2222bbbb2222b" # Account B's VPC ID
peer_owner_id = "222222222222" # Account B's Account ID
peer_region = "us-east-1"
tags = {
Name = "ecs-to-rds-cross-account"
}
}
# ─── Account B (RDS) ───
resource "aws_vpc_peering_connection_accepter" "rds_accept" {
provider = aws.account_b
vpc_peering_connection_id = aws_vpc_peering_connection.ecs_to_rds.id
auto_accept = true
}
# ─── DNS Resolution (both sides) ───
resource "aws_vpc_peering_connection_options" "requester_dns" {
vpc_peering_connection_id = aws_vpc_peering_connection.ecs_to_rds.id
requester {
allow_remote_vpc_dns_resolution = true
}
}
resource "aws_vpc_peering_connection_options" "accepter_dns" {
provider = aws.account_b
vpc_peering_connection_id = aws_vpc_peering_connection.ecs_to_rds.id
accepter {
allow_remote_vpc_dns_resolution = true
}
}
# ─── Route Tables ───
resource "aws_route" "ecs_to_rds_route" {
route_table_id = aws_route_table.ecs_private.id
destination_cidr_block = "10.1.0.0/16"
vpc_peering_connection_id = aws_vpc_peering_connection.ecs_to_rds.id
}
resource "aws_route" "rds_to_ecs_route" {
provider = aws.account_b
route_table_id = aws_route_table.rds_private.id
destination_cidr_block = "10.0.0.0/16"
vpc_peering_connection_id = aws_vpc_peering_connection.ecs_to_rds.id
}
# ─── Security Groups ───
resource "aws_security_group_rule" "rds_allow_ecs" {
provider = aws.account_b
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
security_group_id = aws_security_group.rds_sg.id
description = "Allow PostgreSQL from ECS in Account A"
}
⚖️ VPC Peering vs. Alternatives
| Feature | VPC Peering | AWS PrivateLink + NLB | Transit Gateway | VPC Lattice |
| Complexity | Low | Medium-High | Medium | Medium |
| Cost | Free to create; data transfer charges | NLB + PrivateLink hourly + data | TGW hourly + data | Per-request pricing |
| Overlapping CIDRs | Not supported | Supported | Not supported | Supported |
| Scope of exposure | Entire VPC-to-VPC | Only the specific service | Entire VPC-to-VPC | Only the specific resource |
| Scalability | 1:1 connections (max 125 per VPC) | Many consumers per endpoint | Hub-and-spoke (thousands of VPCs) | Service mesh approach |
| Cross-region | Supported | Requires peering for cross-region | Supported (with peering) | Supported |
| Best for | Simple, low-cost 1:1 connections | Granular access, overlapping CIDRs | Large orgs, many VPCs | Modern service-to-service mesh |
When to choose VPC Peering: You have a small number of VPCs with non-overlapping CIDRs and want the simplest, cheapest option for direct connectivity.
When NOT to choose VPC Peering: Your CIDRs overlap, you need fine-grained service-level access control, or you have dozens of VPCs to interconnect.
⚠️ Common Pitfalls & Gotchas
-
Forgetting DNS resolution — The #1 issue. Without enabling DNS resolution on the peering connection, the RDS endpoint resolves to its public IP and traffic goes out to the internet (and gets blocked by security groups). Always enable DNS resolution on both sides.
-
Route tables not updated on all subnets — If your ECS tasks can launch in multiple subnets, every associated route table needs the peering route. Miss one, and tasks in that subnet can't reach RDS.
-
ECS
awsvpcmode security group — Inawsvpcnetwork mode (required for Fargate), the task-level security group controls network access, not the EC2 host security group. Make sure the task SG allows outbound to the RDS port. -
RDS IP changes after modifications — If you upgrade, resize, or failover an RDS instance, its private IP can change. Always use the DNS endpoint, never hardcode IPs. With DNS resolution enabled on peering, the endpoint will correctly resolve to the new private IP.
-
Transitive peering is not supported — If VPC A peers with VPC B, and VPC B peers with VPC C, VPC A cannot reach VPC C through VPC B. Each pair needs its own peering connection (or use Transit Gateway).
-
Cross-account security group references — You can reference a peer VPC's security group in rules only for same-region peering. For cross-region peering, use CIDR blocks.
-
Secrets Manager cross-account access — If your DB credentials are stored in Account B's Secrets Manager, you need a resource policy on the secret granting access to Account A's ECS task role, plus a KMS key policy if using a CMK.
📎 Useful AWS CLI Commands
# List all peering connections
aws ec2 describe-vpc-peering-connections
# Check peering connection status
aws ec2 describe-vpc-peering-connections \
--vpc-peering-connection-ids pcx-0abc123def456ghi7 \
--query 'VpcPeeringConnections[0].Status'
# Verify route tables
aws ec2 describe-route-tables \
--route-table-ids rtb-0aaa1111 \
--query 'RouteTables[0].Routes'
# Check security group rules
aws ec2 describe-security-groups \
--group-ids sg-0bbb2222rds \
--query 'SecurityGroups[0].IpPermissions'
# Delete a peering connection
aws ec2 delete-vpc-peering-connection \
--vpc-peering-connection-id pcx-0abc123def456ghi7
📚 References
-
AWS Blog — Access RDS Across VPCs Using PrivateLink (alternative)
-
AWS Blog — VPC Lattice for Cross-Account RDS (newer alternative)
Last updated: March 2026