Drei Peerings, 10 Dollar im Monat: Wann VPC Peering Transit Gateway schlägt

aws networking terraform cost-optimization

Wir betreiben eine kleine AWS Organization. Ein ops/internal Account hält eine Handvoll geteilter interner Dienste. Drei Workload-Accounts (dev, staging, prod) haben jeweils eine eigene VPC und müssen diese Dienste erreichen. Eine Region, us-east-1, drei AZs jeweils. Die AWS-Referenzarchitekturen zeigen alle auf Transit Gateway. Ich habe es gegen unseren tatsächlichen Traffic durchgerechnet und mich stattdessen für VPC Peering entschieden. Hier ist die Begründung mitsamt den Zahlen.

Das Problem

Vier AWS-Accounts unter einer Organization:

  • ops-internal: Shared-Services-VPC, 172.16.0.0/16. Beherbergt eine kleine Menge interner Tools — Observability, Secret-Management, internes Connectivity-Control-Plane. Drei Dienste insgesamt.
  • dev: 172.17.0.0/16
  • staging: 172.18.0.0/16
  • prod: 172.19.0.0/16

Die Workload-VPCs müssen die ops-VPC erreichen. Sie müssen nicht miteinander reden — dev soll prod nicht sehen, prod nicht dev. Hub-and-Spoke mit der ops-VPC in der Mitte.

Constraints:

  • Kleines Team. Was ich auch wähle, ich pflege es selbst.
  • Eine Region, keine Expansionspläne für mindestens ein Jahr.
  • Kostensensitiv. Wir sind ein Startup; jeder wiederkehrende Posten auf der AWS-Rechnung muss sich rechtfertigen.
  • Alle vier Accounts laufen bereits unter AWS Organizations, Cross-Account-IAM ist also einfach.

So weit das Setup. Die interessante Entscheidung ist die Connectivity-Schicht.

Die Optionen, die ich erwogen habe

OptionWas es istSweet SpotKostenmodell
VPC PeeringDirekter 1:1-Link zwischen zwei VPCsWenige VPCs, kein transitives Routing nötig0 $/h, nur Datentransfer
Transit GatewayRegionaler Router, Hub-and-SpokeViele VPCs, transitives Routing, zentrale Inspection0,05 $/h pro Attachment + 0,02 $/GB
Site-to-Site VPNIPSec-TunnelHybrid (On-Prem ↔ AWS)0,05 $/h pro Connection + Data Out
PrivateLinkNLB-basierte Service-EndpointsSpezifische Dienste über Accounts hinweg freigeben0,01 $/h pro Endpoint pro AZ + 0,01 $/GB
VPC LatticeService Mesh auf ApplikationsebeneViele Dienste, identitätsbasierte Auth0,025 $/h pro Service + 0,025 $/GB
Userspace-Overlay (Tailscale-Style)WireGuard-Mesh über beliebigem UnderlayApp-Layer-Connectivity für teilnehmende HostsKostenlos / self-hosted

Die Overlay-Option verdient eine eigene Anmerkung. Ein WireGuard-Mesh ersetzt VPC Peering nicht; es sitzt auf dem vorhandenen Underlay und ist nur für Hosts nutzbar, die am Mesh teilnehmen. Für Maschine-zu-Maschine-Traffic zwischen EC2-Instanzen, die den Agent nicht laufen lassen — Prometheus-Scrapes, interne API-Calls — brauchst du weiterhin VPC-Level-Connectivity. Es als einzige Connectivity-Schicht auszuschließen war einfach: zu viele Dinge müssten Mesh-aware sein.

Site-to-Site VPN ist für Hybrid (Rechenzentrum zu AWS). Es intra-AWS einzusetzen ist zu teuer und falsch geschnitten — du zahlst für Tunnel und Customer-Gateway-Betrieb, um ein Problem zu lösen, das AWS bereits mit Peering löst. Ich nehme es der Vollständigkeit halber auf.

Bleiben vier ernsthafte Kandidaten: Peering, TGW, PrivateLink, Lattice.

Warum ich VPC Peering gewählt habe

Vier VPCs in Hub-and-Spoke heißt drei Peering Connections. Mehr nicht. Die n*(n-1)/2-Skalierungswarnung, die alle wiederholen, greift nur, wenn jede VPC mit jeder anderen reden muss. In Hub-and-Spoke mit einem Hub sind es n-1.

Tradeoffs, die ich wissentlich akzeptiert habe:

  • Kein transitives Routing. Sollte dev jemals staging erreichen müssen, brauche ich ein viertes Peering oder muss umdenken. Heute braucht es das nicht, und das heutige Problem ist das einzige, das ich löse.
  • Manuelle Route-Table-Einträge auf beiden Seiten. Beide VPCs eines Peerings brauchen explizite Routen auf die pcx-*-ID. Eine Seite vergessen und du bekommst ein stilles Black-Hole (mehr dazu unten).
  • CIDRs dürfen sich nicht überlappen. Ich habe den Adressraum vorab geplant — angrenzende /16er unter einem /14-Supernet — also einmaliger Aufwand.
  • Kein zentraler Egress, keine Inspection, kein Firewall-Hop. Für uns akzeptabel; wir haben kein SecOps-Team, das einen Transit-Inspection-Point vorschreibt.

Der Punkt, der es entschieden hat: bei vier VPCs kostet allein die TGW-Attachment-Gebühr ~146 $/Monat, bevor auch nur ein Byte fließt. Peering kostet 0 $/Monat im Leerlauf. Das Argument „TGW skaliert besser” stimmt, ist aber nicht umsonst — und wir sind nicht in der Größenordnung, in der die operative Vereinfachung 146 $/Monat wert ist.

Setup-Walkthrough

Drei Terraform-Highlights. Das Muster wiederholt sich für jeden Spoke.

Provider-Aliase für Cross-Account. Die Peering Connection lebt auf der Requester-Seite (ops); die Accepter-Resource auf der Workload-Seite. Beide brauchen explizite Provider — mein erster Versuch hatte den aws_vpc_peering_connection_accepter versehentlich unter dem Requester-Provider laufen. Terragrunt hat fröhlich applied; der Accepter ist nie gelaufen:

provider "aws" {
  alias  = "ops"
  region = "us-east-1"
  assume_role { role_arn = "arn:aws:iam::${var.ops_account_id}:role/TerragruntExec" }
}

provider "aws" {
  alias  = "workload"
  region = "us-east-1"
  assume_role { role_arn = "arn:aws:iam::${var.workload_account_id}:role/TerragruntExec" }
}

Requester-Seite (ops-Account). Setze peer_region bei Same-Region-Peerings nicht — das Argument ist nur für Inter-Region gedacht, und es zu setzen kippt AWS in den Cross-Region-Modus (andere Bepreisung, andere Option-Block-Regeln). Das auto_accept-Flag hier greift nur, wenn beide VPCs im selben Account liegen; bei Cross-Account musst du eine separate aws_vpc_peering_connection_accepter-Resource unter dem Provider des Accepters deklarieren:

resource "aws_vpc_peering_connection" "ops_to_workload" {
  provider      = aws.ops
  vpc_id        = aws_vpc.ops.id
  peer_vpc_id   = var.workload_vpc_id
  peer_owner_id = var.workload_account_id
  auto_accept   = false
  # kein peer_region — Same-Region-Peering

  tags = { Name = "ops-to-${var.workload_env}" }
}

Accepter-Seite (Workload-Account):

resource "aws_vpc_peering_connection_accepter" "workload_from_ops" {
  provider                  = aws.workload
  vpc_peering_connection_id = aws_vpc_peering_connection.ops_to_workload.id
  auto_accept               = true

  tags = { Name = "from-ops" }
}

Route-Table-Einträge — beide Seiten:

resource "aws_route" "ops_to_workload" {
  provider                  = aws.ops
  route_table_id            = aws_route_table.ops_private.id
  destination_cidr_block    = var.workload_vpc_cidr
  vpc_peering_connection_id = aws_vpc_peering_connection.ops_to_workload.id
}

resource "aws_route" "workload_to_ops" {
  provider                  = aws.workload
  route_table_id            = var.workload_private_rt_id
  destination_cidr_block    = aws_vpc.ops.cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.ops_to_workload.id
}

Der Gotcha, der 30 Minuten meines Lebens gefressen hat. Erstes Peering ging auf ACTIVE. Security Groups waren konfiguriert. Traffic ging ins Leere. Kein Fehler, kein ICMP unreachable, einfach stille Packet Drops. Ich hatte den Route-Table-Eintrag auf der Accepter-Seite vergessen. Der Zustand des Peerings und der Zustand des Routings sind unabhängig; AWS meldet das Peering bereitwillig als gesund, während der eigentliche Traffic nirgendwohin kann.

Zweiter Gotcha: DNS. Interne Hostnames lösten von den Workload-Accounts aus auf öffentliche IPs auf, bis ich Remote-DNS-Resolution aktiviert hatte. Bei Cross-Account-Peerings muss diese Option von jeder Seite aus dem eigenen Account gesetzt werden — der Requester kann das Accepter-Flag nicht setzen und umgekehrt. Heißt: zwei separate Resources, jede unter dem richtigen Provider, jede mit der richtigen Peering-ID referenziert (die Accepter-Resource referenziert die ID des Accepters, nicht die des Requesters):

resource "aws_vpc_peering_connection_options" "requester" {
  provider                  = aws.ops
  vpc_peering_connection_id = aws_vpc_peering_connection.ops_to_workload.id
  requester { allow_remote_vpc_dns_resolution = true }
}

resource "aws_vpc_peering_connection_options" "accepter" {
  provider                  = aws.workload
  vpc_peering_connection_id = aws_vpc_peering_connection_accepter.workload_from_ops.id
  accepter  { allow_remote_vpc_dns_resolution = true }
}

Im Nachhinein hätte ich am ersten Tag ein peering-pair-Modul bauen sollen — Requester, Accepter, beide Route-Einträge, beide DNS-Option-Blöcke, alles hinter einer Eingabeschnittstelle. Ich habe das erste inline geschrieben, weil „es ist ja nur ein Peering”, für das zweite und dritte kopiert, und jetzt sitze ich auf drei Peerings mit unveränderter Duplikation. Refactoring wäre ein Terraform-State-Move-Tanz, den ich nicht priorisiert habe. Klassiker.

Kostenvergleich mit echten Zahlen

Szenario: 3 Workload-VPCs ↔ 1 Shared-Services-VPC, us-east-1, ~500 GB/Monat Cross-VPC-Traffic, 3 AZs. Alle Preise sind auf die AWS-Pricing-Seiten verlinkt, damit du sie nachvollziehen kannst, wenn (nicht falls) sie sich ändern.

VPC Peering. 0 $/h für die Connection selbst. Datentransfer ist der einzige Posten. AWS berechnet Cross-AZ-Transfer doppelt — 0,01 $/GB auf dem Account des Senders (out) plus 0,01 $/GB auf dem Account des Empfängers (in), also 0,02 $/GB kombiniert pro übertragenem GB. Bei ~500 GB/Monat über alle drei Peerings: ~10 $/Monat. Weniger, wenn du Dienste an AZs pinnst, aber ich bleibe ehrlich beim Worst Case. (VPC-Preise)

Transit Gateway. 4 Attachments × 0,05 $/h × 730 h = 146 $/Monat allein für Attachments. Plus 0,02 $/GB processed × 500 GB = 10 $. ~156 $/Monat vor allem anderen. Die gleichen ~10 $ Cross-AZ-Datentransfer würden bei TGW genauso anfallen — der echte Unterschied sind die 146 $/Monat Attachment-Gebühren, nicht die Datenebene. (TGW-Preise)

Site-to-Site VPN. 0,05 $/h pro Connection × 730 h × 3 Connections = 109,50 $/Monat, plus Data Transfer Out und der operative Aufwand für Customer Gateways. ~110 $/Monat und das falsche Werkzeug für intra-AWS. (VPN-Preise)

PrivateLink. Per-Endpoint-Preise explodieren bei mehreren Diensten schnell. Drei Services × 3 AZs × 0,01 $/h × 730 = 65,70 $/Monat pro Consumer-VPC. Drei Workload-VPCs als Consumer = 197 $/Monat allein für Endpoints, plus 0,01 $/GB × 500 GB = 5 $. ~202 $/Monat. (PrivateLink-Preise)

VPC Lattice. 0,025 $/h pro Service × 3 Services × 730 = 54,75 $/Monat, plus 0,025 $/GB × 500 GB = 12,50 $. ~67 $/Monat. (Lattice-Preise)

OptionMonatliche Kosten (dieses Szenario)
VPC Peering~10 $
VPC Lattice~67 $
Site-to-Site VPN~110 $
Transit Gateway~156 $
PrivateLink~202 $

Break-Even mit TGW. Datentransfer pro GB ist auf beiden Seiten etwa gleich (~0,02 $/GB). Die Entscheidung kippt komplett über die Attachment-Gebühr gegen den operativen Aufwand von n-1 Peerings (Hub-and-Spoke) bzw. n*(n-1)/2 (Full Mesh). Meine Faustregel: ab ~5 VPCs in Hub-and-Spoke oder bei Full-Mesh-Anforderung wechselst du auf TGW. Darunter gewinnt Peering bei den Kosten um eine Größenordnung, und der operative Mehraufwand sind „zwei Route-Table-Einträge mehr”.

Wann du Peering NICHT nutzen solltest

  • Mehr als ~5 VPCs. Die n*(n-1)/2-Kurve macht das Management auch im Hub-and-Spoke schmerzhaft, sobald ein Spoke einen anderen erreichen muss.
  • Transitives Routing. Peerings transportieren nicht weiter. Wenn A↔B und B↔C besteht, erreicht A trotzdem nicht C ohne A↔C. TGW löst das nativ.
  • Häufige CIDR-Änderungen. Jede Änderung ist ein Koordinations-Event über Accounts hinweg.
  • Zentraler Egress oder Inspection. Wenn Compliance eine Inspection-VPC oder zentrales NAT verlangt, brauchst du TGW oder ein Transit-VPC-Pattern.
  • Cross-Region in großem Maßstab. Inter-Region-Peering geht, aber Datenkosten und operativer Aufwand klettern schnell. TGW-Peering über Regionen ist die bessere Form.
  • Service-Level-Zugriffskontrolle statt Netzwerkebene. Das ist PrivateLink- oder Lattice-Territorium.

Takeaways

  • Default-zu-TGW ist eine Cargo-Cult-Entscheidung für kleine Teams. Die AWS-Referenzarchitekturen sind für Unternehmen mit Dutzenden Accounts und eigenem Network-Team geschrieben. Du bist (vermutlich) nicht die.
  • Die Warnung „Peering skaliert nicht” ist wahr, aber falsch formuliert. Es skaliert nicht über ~5 VPCs in einem Full Mesh hinaus. Bei 4 VPCs in Hub-and-Spoke sind das drei Connections — langweilig, günstig, erledigt.
  • Rechne Connectivity-Kosten immer auf deinem tatsächlichen Traffic-Profil. Der Break-Even-Punkt wird von Attachment-Stunden dominiert, nicht von Daten — und die Defaults im AWS-Calculator lenken dich auf TGW, selbst wenn du mit Peering 140 $/Monat sparen würdest.
  • Bau das peering-pair am ersten Tag als wiederverwendbares Terraform-Modul. Schreib das erste nicht inline, weil „es ist ja nur eins”. Ich habe genau das gemacht und zahle dafür immer noch in Copy-Paste.
  • Die stillen Fehlermodi sind die echten Kosten. Peering plus vergessene Route-Table-Einträge plus fehlende DNS-Option-Flags fressen einen Nachmittag. Pack sie ins Modul und denk nie wieder dran.