Transit Gatewayを「繋いだら終わり」にしてた過去の自分を殴りたい話

VPCピアリングの限界でTGWを導入したら、ルートテーブル設計ミス・無駄なアタッチメント費用・マルチリージョン遅延の三重苦に。2年運用して気づいた失敗と改善の記録です。

TGWを「とりあえず繋いだら終わり」にしてた過去の自分を殴りたい

うちのチームがTransit Gateway(以下TGW)を本格導入したのは2023年後半で、当時は「VPCピアリングの管理が限界になった」という切実な理由からだった。最初のうちは「繋がればいい」で設計してたんだけど、2年以上運用してみるとルートテーブルの設計ミス・不必要なアタッチメントのコスト・マルチリージョン接続の遅延という三重苦が徐々に効いてきた。

2026年に入ってNetwork Manager周りの機能がだいぶ成熟したこともあり、今年頭にがっつり設計を見直した。その過程で気づいたこと、ハマったこと、「これ最初から知りたかった」という知見を整理して書く。マルチクラウド戦略を検討している方にも関連する話なので参考になれば。


実際の構成と設計の全体像

まず現時点のうちの構成を共有する。AWS Organizationsで管理している15アカウント、3リージョン(ap-northeast-1 / us-east-1 / eu-west-1)で動いている。

graph TB
    subgraph OrgRoot["AWS Organizations"]
        subgraph NetworkAccount["ネットワーク集約アカウント"]
            TGW_AP["TGW\nap-northeast-1"]
            TGW_US["TGW\nus-east-1"]
            TGW_EU["TGW\neu-west-1"]
            TGW_AP <-->|"TGW Peering"| TGW_US
            TGW_AP <-->|"TGW Peering"| TGW_EU
        end

        subgraph AP_Region["ap-northeast-1"]
            subgraph SharedServiceVPC["Shared Services VPC / 10.0.0.0/16"]
                subgraph AZ1_SS["AZ-a"]
                    DNS["Route53 Resolver"]
                    NATGW1["NAT Gateway"]
                end
                subgraph AZ2_SS["AZ-c"]
                    NATGW2["NAT Gateway"]
                end
                Endpoint["VPC Endpoints\n(S3/ECR/SSM)"]
            end

            subgraph ProdVPC["Production VPC / 10.1.0.0/16"]
                subgraph AZ1_P["AZ-a"]
                    ECS_P["ECS Cluster"]
                    RDS_P["RDS Primary"]
                end
                subgraph AZ2_P["AZ-c"]
                    ECS_P2["ECS Cluster"]
                    RDS_S["RDS Standby"]
                end
            end

            subgraph StagingVPC["Staging VPC / 10.2.0.0/16"]
                ECS_S["ECS Cluster"]
            end

            subgraph DevVPC["Dev VPC / 10.3.0.0/16"]
                EC2_D["EC2 Dev"]
            end
        end

        TGW_AP --> SharedServiceVPC
        TGW_AP --> ProdVPC
        TGW_AP --> StagingVPC
        TGW_AP --> DevVPC
    end

    IGW["Internet Gateway"] --> SharedServiceVPC
    NATGW1 --> IGW
    NATGW2 --> IGW

この構成のポイントはShared Services VPCを経由させる「ハブアンドスポーク」アーキテクチャにしていること。NAT GatewayをShared Services VPCに集約することで、各環境VPCにNAT Gatewayを置くコストを削減している。これが最初の大きな改善点だった。

ただしここに落とし穴があって、後述する。


ルートテーブル設計:フラット構成の罠

TGWを最初に設定したとき、ルートテーブルを1個だけ作って全アタッチメントにアソシエートするいわゆる「フラット構成」にしていた。正直この頃は「とりあえず繋がれば」という状態だった。

これの何が問題だったかというと、Dev環境からProductionデータベースに直接アクセスできる状態になっていたこと。セキュリティレビューで指摘されるまで気づかなかった。AWS Organizations運用でSCPを整備していたが、ネットワーク層の分離は完全に抜けていた。恥ずかしい話だが、「SCPで制御してるから大丈夫」という慢心があったんだと思う。

2026年現在、うちのチームは以下の3ルートテーブル構成に移行している。

graph LR
    subgraph TGW_RT["TGW ルートテーブル設計"]
        RT_SHARED["RT-Shared\n共通サービス用"]
        RT_PROD["RT-Production\n本番隔離用"]
        RT_DEV["RT-Dev/Staging\n非本番用"]
    end

    RT_SHARED --> |"Propagation"| ATTACH_SHARED["Shared Services VPC"]
    RT_PROD --> |"Association"| ATTACH_PROD["Prod VPC"]
    RT_DEV --> |"Association"| ATTACH_DEV["Dev/Staging VPC"]

    ATTACH_PROD --> |"共通サービスへのルートのみ"| RT_SHARED
    ATTACH_DEV --> |"共通サービスへのルートのみ"| RT_SHARED

実際のルートテーブル設定をTerraformで書くとこんな感じ。

# Production用ルートテーブル
resource "aws_ec2_transit_gateway_route_table" "prod" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id

  tags = {
    Name = "tgw-rt-production"
    Env  = "production"
  }
}

# ProdルートテーブルにはShared ServicesへのルートのみStatic追加
resource "aws_ec2_transit_gateway_route" "prod_to_shared" {
  destination_cidr_block         = "10.0.0.0/16"
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.shared_services.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.prod.id
}

# Prod VPCからのトラフィックはRT-Prodに関連付け
resource "aws_ec2_transit_gateway_route_table_association" "prod" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.prod.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.prod.id
}

# Dev/StagingルートテーブルにはBlackhole for Prod
resource "aws_ec2_transit_gateway_route" "dev_blackhole_prod" {
  destination_cidr_block         = "10.1.0.0/16" # Prod CIDR
  blackhole                      = true
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.dev.id
}

このblackholeルートがポイントで、Dev→Prodのパケットを明示的に捨てる。設定してみると「なんでもっと早くやらなかったんだ」という気持ちになる。地味に重要な設定なのに、公式ドキュメントを読んでいるだけだと重要性が伝わりづらいんだよな。


コスト最適化:気づいたら月30万円のTGW請求になってた

正直これが一番痛かった話をする。

TGWのコスト構造を改めておさらいしておこう。2026年時点(東京リージョン)での料金はこんな感じだ。

コスト項目単価(東京)備考
アタッチメント時間課金$0.07/時間/アタッチメント常時接続分
データ処理料金$0.02/GBTGW経由の全トラフィック
TGWピアリング(リージョン間)$0.07/GBリージョン間データ転送
Network Manager$0.05/接続/時間オプション

うちが月30万円に達していた原因を調べたら、すべてのVPCアタッチメントが24時間接続されていた + 不要なリージョン間レプリケーションがTGW経由を通っていたという組み合わせだった。個人的には後者のほうがダメージが大きかった。リージョン間はデータ転送料金まで乗っかってくるので、気づいたときには「あ、これやばい」と声が出た。

具体的にやった削減施策を3つ紹介する。

1. 開発環境のアタッチメントをスケジュール制御

開発環境のVPCはその名の通り開発時間帯しか使わないのに、24時間アタッチメントを維持していた。EventBridge + LambdaでTGWアタッチメントをON/OFFする仕組みを作ったら、アタッチメント費用が月次で約35%削減できた。

import boto3
import os

ec2 = boto3.client('ec2')

def handler(event, context):
    action = event.get('action')  # 'attach' or 'detach'
    attachment_id = os.environ['DEV_TGW_ATTACHMENT_ID']
    
    if action == 'detach':
        # アタッチメントを削除(ただしVPCルートテーブルからも削除する必要あり)
        response = ec2.delete_transit_gateway_vpc_attachment(
            TransitGatewayAttachmentId=attachment_id
        )
        print(f"Detached: {response['TransitGatewayVpcAttachment']['State']}")
    elif action == 'attach':
        # 再アタッチ
        response = ec2.create_transit_gateway_vpc_attachment(
            TransitGatewayId=os.environ['TGW_ID'],
            VpcId=os.environ['DEV_VPC_ID'],
            SubnetIds=os.environ['DEV_SUBNET_IDS'].split(','),
            Options={
                'DnsSupport': 'enable',
                'Ipv6Support': 'disable',
                'ApplianceModeSupport': 'disable'
            },
            TagSpecifications=[{
                'ResourceType': 'transit-gateway-attachment',
                'Tags': [{'Key': 'Name', 'Value': 'dev-vpc-attachment'}]
            }]
        )
        print(f"Attached: {response['TransitGatewayVpcAttachment']['State']}")
    
    return {'status': 'ok'}

注意点として、アタッチメントを削除するとVPC側のルートテーブルのTGWエントリも消える。再アタッチ時に自動で復元するスクリプトも合わせて実装が必要だった。ここは地味に面倒くさい部分で、「Lambdaが1個あれば終わり」とはいかないので過信は禁物。

2. S3・ECR等はVPC Endpointへ逃がしてTGW通過をゼロに

Shared Services VPC経由のインターネットアクセスをNAT GatewayとTGWで実現していたが、S3やECRへのトラフィックが全部TGWを経由していたのが盲点だった。これ、意識しないと本当に気づかない。

Gateway型のVPC EndpointはS3とDynamoDBに対応しており、これらはコスト無料で設定できる。Interface型のEndpointはECR・SSM・Secrets Managerなど対応しており、時間課金あり($0.014/時間)だがTGWデータ処理料($0.02/GB)より安くなるケースが多い。

CloudWatch メトリクスで BytesIn / BytesOut を確認して、S3とECRへのトラフィックをVPC Endpointに逃がしたところ、月のデータ処理費用が約40%削減できた。やらない理由が本当にない施策だった。

3. Transit Gateway Flow Logsで可視化してから削る

「なんとなくコスト高い」の状態から脱するのに、2026年から正式にGAになっているTGW Flow Logsを活用した。VPCフローログと似た形式でアタッチメント単位のトラフィックが取れる。

# TGW Flow Logs有効化(Athenaで分析するためS3へ)
aws ec2 create-flow-logs \
  --resource-type TransitGateway \
  --resource-ids tgw-xxxxxxxxxxxxxxxxx \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::my-tgw-flowlogs/tgw/ \
  --log-format '${version} ${resource-type} ${account-id} ${tgw-id} ${tgw-attachment-id} ${tgw-src-vpc-account-id} ${tgw-dst-vpc-account-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${log-status} ${type} ${packets-lost-no-route} ${packets-lost-blackhole} ${packets-lost-mtu-exceeded} ${packets-lost-ttl-expired}'

Athenaで分析するクエリも載せておく。どのアタッチメントが一番トラフィックを消費しているか確認できる。

SELECT 
  tgw_attachment_id,
  SUM(bytes) / 1024 / 1024 / 1024 AS total_gb,
  COUNT(*) AS flow_count
FROM tgw_flow_logs
WHERE dt = '2026-05-01'
  AND log_status = 'OK'
GROUP BY tgw_attachment_id
ORDER BY total_gb DESC
LIMIT 20;

これで「StagingからS3への定期バックアップ処理がTGW経由だった」という想定外のトラフィックが発覚した。感覚で削減施策を打つより、まずFlow Logsで実態を把握することを強くすすめる。VPCフローログ運用の記事も参考になると思う。


2026年のNetwork Manager統合:ようやく実用レベルになった

最初にNetwork Managerが出た頃は「可視化できるだけ」という印象で正直あまり使っていなかった。が、2025年後半から2026年にかけてReachability Analyzerとの深い統合・Route Analyzer改善・イベントドリブンなアラートが整備されて、ようやく実戦投入に値すると判断した。

特に便利だったのがRoute Analyzerで、「特定VPCから別VPCへのルートが通るか」をコンソールやAPIで確認できる。

# Route Analyzerでルート検証
aws networkmanager start-route-analysis \
  --global-network-id global-network-xxxxxxxxxx \
  --source '{"TransitGatewayAttachmentArn": "arn:aws:ec2:ap-northeast-1:123456789012:transit-gateway-attachment/tgw-attach-xxxxxx", "TransitGatewayArn": "arn:aws:ec2:ap-northeast-1:123456789012:transit-gateway/tgw-xxxxxx"}' \
  --destination '{"TransitGatewayAttachmentArn": "arn:aws:ec2:ap-northeast-1:123456789012:transit-gateway-attachment/tgw-attach-yyyyyy", "TransitGatewayArn": "arn:aws:ec2:ap-northeast-1:123456789012:transit-gateway/tgw-xxxxxx"}'

# 結果確認
aws networkmanager get-route-analysis \
  --global-network-id global-network-xxxxxxxxxx \
  --route-analysis-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

返ってくるレスポンスにはルートパスの各ホップと、ブロックされている場合の原因(セキュリティグループ・NACLなど)が含まれる。

{
  "RouteAnalysis": {
    "Status": "completed",
    "ForwardPath": {
      "CompletionStatus": {
        "ResultCode": "CONNECTED"
      },
      "Path": [
        {
          "Sequence": 1,
          "Resource": {
            "ResourceType": "TRANSIT_GATEWAY_ATTACHMENT"
          },
          "DestinationCidrBlock": "10.1.0.0/16"
        }
      ]
    }
  }
}

「なんで繋がらないんだ」デバッグが劇的に楽になった。マジで助かった、という表現しか出てこないレベルで便利なので、まだ使ったことがない方はぜひ試してほしい。


パフォーマンスチューニング:Equal Cost Multi-Path(ECMP)の活用

正直まだ検証中の部分もあるけど、2026年時点でうちのチームが試しているECMP設定について共有する。

TGWはデフォルトでECMPをサポートしており、複数のVPN/Direct Connect アタッチメントがある場合に帯域幅をスケールアウトできる。ただしVPCアタッチメントのECMPは少し仕様が違う。

VPCアタッチメントの場合、アタッチする際に複数のAZのサブネットを指定するほど帯域が上がる。最初1つのAZのサブネットだけ指定していたが、3AZに展開したところスループットが約3倍になった。数値でまとめるとこんな感じだ。

xychart-beta
    title "TGW スループット比較(Gbps)"
    x-axis ["単一AZ\nサブネット", "2AZ\nサブネット", "3AZ\nサブネット", "ECMP有効\n(VPN×2)"]
    y-axis "スループット (Gbps)" 0 --> 50
    bar [10, 20, 30, 45]

パフォーマンス問題が起きてから気づくケースが多いと思うが、最初からマルチAZで設計しておくべきだったと反省している。

# マルチAZで指定することで帯域幅スケール
resource "aws_ec2_transit_gateway_vpc_attachment" "prod" {
  subnet_ids         = [
    aws_subnet.prod_az_a.id,
    aws_subnet.prod_az_c.id,
    aws_subnet.prod_az_d.id  # 3AZ
  ]
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.prod.id

  options = {
    dns_support                                     = "enable"
    ipv6_support                                    = "disable"
    appliance_mode_support                          = "disable"
    security_group_referencing_support              = "enable"  # 2025年にGAした機能
  }
}

security_group_referencing_supportは2025年後半にGA。これを有効にすると、異なるVPCのセキュリティグループをソース/デスティネーションとして直接参照できる。VPCピアリング時代には普通にできていたのに、TGW環境で長らく使えなかった機能がやっと来た感じで、個人的にはかなり嬉しいアップデートだった。CIDRブロック管理の煩雑さが大幅に減るので、うちは積極的に使っている。好みは分かれるかもしれないけど。


まとめ

TGWの最適化を1年かけてやってきた中で、特に重要だと感じた点をまとめる。

  1. ルートテーブルを環境別に分離せよ:フラット構成は「とりあえず動く」けど、セキュリティとコスト管理の両面でいずれ破綻する。Blackholeルートを使った明示的な遮断が大事

  2. TGW Flow Logsで先に可視化してから削る:感覚でコスト削減しようとすると的外れな施策になる。Flow Logs + Athenaでどのアタッチメントが何GBを消費しているか把握してから動く

  3. S3/DynamoDBはGateway Endpointで必ずTGWを回避:無料で設定できるのに使わない理由がない。TGWデータ処理料$0.02/GBを節約できる最も費用対効果が高い施策

  4. Network ManagerのRoute Analyzerは実用レベルになった:2026年時点でデバッグツールとして十分使える。TGW設計を変更するたびにRoute Analyzerで疎通確認を自動テストするCIに組み込むのもあり

  5. アタッチメントの3AZ展開でスループット3倍:パフォーマンス問題が起きてから気づくケースが多いが、最初からマルチAZで設計しておくべきだった

もし「TGWコスト高いな」と感じている方がいれば、まずTGW Flow Logsを有効化してAthenaで分析することを強くすすめる。うちは最初の調査で「想定外の巨大トラフィック源」が2つ見つかった。皆さんの環境ではどんな意外なトラフィックが流れてますか?

インシデント時の対応との組み合わせについてはインシデント対応ベストプラクティスも参考になると思う。また、Terraformでのネットワーク設計全般についてはTerraformの設計論に実践的な知見がまとまっているので、合わせて読んでみてほしい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事