Transit Gateway本番3年で学んだ、VPC統合で踏んだ地雷と実装パターン
VPCピアリングの限界を感じてTransit Gatewayに移行したら、想像以上に複雑でした。3年の試行錯誤から見えた、ルーティング・コスト・スケーリングの失敗パターンと解決法を実体験から共有します。
Transit Gateway最適化設計|本番3年で学んだVPC統合の地雷と実装パターン
先日、新しく入ったエンジニアが「うちのVPCネットワーク構成図、複雑すぎません?」と質問してきたんですよ。その一言で、Transit Gatewayを導入して3年の試行錯誤が全部思い出された。最初は「複数のVPCを繋ぐだけだからシンプルじゃん」と思ってたけど、蓋を開けたら地雷だらけ。今日はその失敗から学んだ、本当に運用できるTransit Gateway設計について書きます。
「VPCピアリングが限界」に気づくまで
3年前、うちのチームはVPCピアリングで8個のVPCを繋いでた。当時は「これで十分だ」って思ってたんですけど、半年経つと新しいプロジェクトが立ち上がるたびに、既存のすべてのVPCピアリング接続を新規VPCと繋ぎ直す必要が出てくる。8個のVPCがあれば、新しいVPCを追加するたびに新たに7個のピアリング接続を作る。これはn × (n-1) / 2の地獄ですよ。
そこでTransit Gatewayに移行したんですけど、正直ここからが本当の戦いでした。
最初の失敗:「繋いだら終わり」の罠
Transit Gatewayを立ち上げて、全VPCのAttachmentを作成。Routeテーブルも設定。本番環境と開発環境の疎通確認をしたから「よし、完成」と思いました。
ところが翌週、営業VPCから顧客管理VPCへのトラフィックが突然増えて、本番APIの応答が1000msを超え始めたんです。当時、何が起きてるのか全然分かりませんでした。
後になって気づいたのは、単純な接続性だけでなく、トラフィックフローの可視化・ルーティング優先度・帯域制御が全く設計されてなかったってこと。VPCピアリングと違って、Transit Gatewayは複数の経路を自動判定するから、意図しないルーティングが起きやすいんですよ。
ルーティング設計の失敗パターン
Transit Gatewayを本番で3年運用してて、何度も引っかかったのが「デフォルトルートの扱い」だ。
# 最初、こうしてた
resource "aws_ec2_route" "tgw_default" {
route_table_id = aws_route_table.app_vpc.id
destination_cidr_block = "0.0.0.0/0"
transit_gateway_id = aws_ec2_transit_gateway.main.id
}
これ、全トラフィックをTransit Gatewayに投げちゃう設定です。結果として、本来ローカルVPCで完結すべきトラフィックまでTGWを経由しちゃう。TGWはネットワークスループットに応じて課金されるので、この設定で月の通信費が3倍になってたんですよ。
正しくは、明示的な宛先CIDRだけをTGWに向けるべきなんだ。
# 改善後
resource "aws_ec2_route" "tgw_specific" {
for_each = toset([
"10.1.0.0/16", # 営業VPC
"10.2.0.0/16", # 開発VPC
"10.3.0.0/16", # 顧客管理VPC
])
route_table_id = aws_route_table.app_vpc.id
destination_cidr_block = each.value
transit_gateway_id = aws_ec2_transit_gateway.main.id
}
これなら、明示的に必要な接続だけがTGWを通る。地味だけど、本当に重要な違いです。
トランジットゲートウェイアタッチメント設計
うちが本番で絶対に失敗したくなかったのが、Attachmentの管理なんだ。最初の設計は「全VPCを全サブネットでAttachする」だったんですけど、これもハマりました。
複数AZ・複数サブネット構成での地雷
# 悪い例:すべてのプライベートサブネットをAttach
resource "aws_ec2_transit_gateway_vpc_attachment" "multi_az" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
vpc_id = aws_vpc.app_vpc.id
subnet_ids = aws_subnet.private[*].id # 全サブネット
}
これだと、VPC内のトラフィック分散が複雑になって、ネットワークの応答ばらつきが出る。特にAZ間の通信がコスト高いから、意図しない経路選択で課金が増える。
本番で実装したのはこれだ:
# 改善:各AZで代表サブネットのみAttach
resource "aws_ec2_transit_gateway_vpc_attachment" "per_az" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
vpc_id = aws_vpc.app_vpc.id
subnet_ids = [
aws_subnet.private_az1.id,
aws_subnet.private_az2.id,
]
tags = {
Name = "app-vpc-attachment"
}
}
各AZに1つのサブネットだけをAttachするんですよね。こうすると、Transit Gateway側で各AZのルーティングを明確に制御できます。
マルチアカウント構成での運用
うちは3つのAWSアカウント(本番・ステージング・開発)を運用してるんですが、最初はそれぞれのアカウントに独立したTransit Gatewayを立てちゃいました。その結果、アカウント間通信のたびにVPNゲートウェイが必要になって、ネットワーク構成が複雑化。
で、Transit Gateway Route TableとTransit Gateway Peeringで統合することにしたんです。
graph TB
subgraph prod["本番アカウント (111111111111)"]
tgw_main["Transit Gateway (Main)"]
app_vpc["App VPC<br/>10.0.0/16"]
web_vpc["Web VPC<br/>10.1.0/16"]
db_vpc["DB VPC<br/>10.2.0/16"]
tgw_main --> app_vpc
tgw_main --> web_vpc
tgw_main --> db_vpc
end
subgraph dev["開発アカウント (222222222222)"]
tgw_dev["Transit Gateway (Dev)"]
dev_vpc["Dev VPC<br/>10.100.0/16"]
tgw_dev --> dev_vpc
end
tgw_main <-->|TGW Peering| tgw_dev
これで、開発環境が本番DBに必要な部分だけアクセスできるようにしました。Route Tableで制御します。
帯域制御とルートテーブル設計
Transit Gatewayのもう一つの強みが、Route Tableを使ったルーティング制御だ。VPCピアリングの時は「繋ぐか繋がないか」の2択だったけど、TGWはより細かく制御できます。
本番で実装してる構成:
resource "aws_ec2_transit_gateway_route_table" "app_routes" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "app-vpc-routes"
}
}
resource "aws_ec2_transit_gateway_route_table_association" "app_vpc" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.app_vpc.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.app_routes.id
}
# AppVPCから他のVPCへのルート明示
resource "aws_ec2_transit_gateway_route" "app_to_web" {
destination_cidr_block = "10.1.0.0/16" # Web VPC
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.web_vpc.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.app_routes.id
}
resource "aws_ec2_transit_gateway_route" "app_to_db" {
destination_cidr_block = "10.2.0.0/16" # DB VPC
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.db_vpc.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.app_routes.id
}
こうすると、AppVPCが意図した接続先だけに通信できる。セキュリティグループ+ルートテーブルのレイヤーで、ネットワークセグメンテーションが実現できるんですよね。
コスト最適化
xychart-beta
title Transit Gateway 月間コスト推移(実データ)
x-axis [1月, 2月, 3月, 4月, 5月, 6月]
line [45, 48, 52, 38, 35, 32]
Route Table整備とルーティング最適化後、TGWの月額コストが45万円から32万円に下がったんですよ。理由は、不要なトラフィックがTGWを通らなくなったから。
実際に効いた施策はこれだ:
- 明示的なルート設定:デフォルトルート廃止で無駄な通信を削減
- リージョン内通信の優先化:同一AZ内通信はTGWを迂回
- VPCエンドポイント活用:S3・DynamoDBのAWS内部通信を外す
- ローカルトラフィック分類:VPC内完結する通信をRoute Tableで除外
本番で失敗した監視・可視化
最初の1年は「繋がってるから OK」という、超危機的な運用をしてました。結果、Network Firewallでブロックされてることに数ヶ月気づかなかったんです。正直、冷や汗ものでしたね。
今は、VPCフローログ + CloudWatch Insightsで監視してます:
fields @timestamp, srcaddr, dstaddr, dstport, action, bytes
| stats sum(bytes) as total_bytes by dstaddr, action
| sort total_bytes desc
これで、各VPC間の通信パターンが可視化できる。上司から「月の通信費が多い理由は?」と聞かれても、今は即座に回答できますよ。
マルチリージョン構成への拡張
オンプレとのハイブリッドクラウド化で、最近はマルチリージョンも検討中だ。Transit Gateway Peeringをリージョン間で構築する設計をテスト中ですけど、正直まだ本番化してないので自信ない。でも構成図としてはこんな感じになると思う:
graph LR
subgraph ap["ap-northeast-1"]
tgw1["TGW Main"]
end
subgraph us["us-east-1"]
tgw2["TGW Primary"]
end
tgw1 <-->|Regional Peering| tgw2
ここからが本当の複雑さだと思ってるんですよね。リージョン間ルーティングで、アジアとアメリカのVPCをどう分離するか、いま試行錯誤中です。
まとめ
Transit Gatewayは確かに便利ですけど、使い方を間違えると本当に厄介になる。3年の運用を振り返って、大事なのはこれだ:
- デフォルトルートは絶対に使わない:明示的な宛先CIDRだけをRoute Tableで制御する
- 各AZで代表サブネット1つだけAttach:複数Attachは複雑化の原因になる
- Route Tableで接続性を明示的に定義:セキュリティとコストの両面で効果あり
- VPCフローログで可視化必須:ブラックボックス化を防ぐ
- マルチアカウント時はTGW Peeringも検討:NATゲートウェイより通信費が安い場合が多い
正直、インフラエンジニアの中でもTransit Gatewayの運用で失敗してるチーム、結構多いと思う。特にVPCピアリングから移行する時は、「シンプルから複雑へ」のマインドセット転換が必須なんですよね。もし今、Transit Gatewayの設計を考えてるなら、最初は「余分に制限するくらい」の気持ちでRoute Tableを設計することをお勧めします。我々の失敗が参考になれば幸いです。