【実務・中級編】 トランジットゲートウェイ(TGW)によるハブ&スポークネットワーク構成 – クラウド&コンテナネットワーク実践ガイド

AWS Transit Gateway(TGW)で実現する、スケールするハブ&スポークネットワーク設計の裏側

こんにちは。数々の修羅場をくぐり抜けてきたインフラエンジニアの皆さんなら、こんな悪夢を一度は経験したことがあるはずです。

「事業の急拡大に伴い、マイクロサービス用のVPCが乱立した結果、VPCピアリングが完全にメッシュ状のスパゲッティ状態になり、ルーティングテーブルの管理が破綻した」
「オンプレミスのデータセンターと5つのAWSアカウント、合計20個のVPCを接続する必要が出てきたが、IPアドレスの重複やルーティングの伝搬制御で夜も眠れない」

こうした「VPCピアリング地獄」から私たちを救い出してくれるのが、AWS Transit Gateway(TGW)を中心としたハブ&スポークアーキテクチャです。

今回は、教科書的な仕様のなぞり書きは一切なしで、パケットがTGWの内部をどう駆け巡り、ルーティングドメイン(ルートテーブル)がいかにしてトラフィックを制御しているのか、現場のリアルな知見を交えて徹底解説します。実務ですぐに使えるTerraformのコードや、パケットロスに直面したときのデバッグ手順まで深掘りしていきましょう。

—

1. なぜ「VPCピアリング地獄」は崩壊するのか? TGWが必要な本当の理由

クラウドネイティブなシステム開発が進むにつれ、開発チームごとに独立したVPCを切り、権限を委譲していくアプローチは定石となりました。しかし、ここで問題になるのが「VPC間の通信」です。

初期の設計では、VPC AからVPC Bへ、VPC BからVPC Cへと個別にVPCピアリング(VPC Peering)を結んでやり過ごせます。しかし、VPCの数が $N$ 個に増えたとき、フルメッシュでピアリングを結ぶと必要な接続数は以下の計算式で爆発的に増加します。

$$\frac{N(N – 1)}{2}$$

VPCが10個あれば45本、20個に達すれば190本のピアリングと、それぞれのVPCルートテーブルへの経路追加・メンテナンスが必要になります。これでは、ルートの伝搬ミスによるルーティングループや、セキュリティ監査の抜け穴が生まれるのは時間の問題です。

TGWによるハブ&スポークの思想

TGWは、いわばクラウド上の超高速レイヤー3ルーターです。
すべてのVPCやオンプレミス(Direct Connect / VPN)を「スポーク」とし、中央のTGWを「ハブ」として1対1でアタッチ(接続)します。

  • 接続の集約: $N$ 個のVPCがある場合、TGWへのアタッチメントは $N$ 個だけで済みます。
  • ルーティングの分離: TGW独自のルートテーブルを複数持たせることで、「本番環境VPC群」「検証環境VPC群」「共有サービスVPC群」といったトラフィックの分離(マルチテナント的な分離)が容易になります。

—

2. TGWの核心:アタッチメントとルートテーブルのメカニズム

TGWを設計する上で最も重要なコンポーネントが、TGWアタッチメント(Attachment)とTGWルートテーブルです。ここを勘違いすると、「なぜかパケットが届かない」「意図しないルートに流れる」という泥沼にハマります。

通信のライフサイクル(パケットはどこを通るか?)

1. 送信元VPC: プライベートサブネットのリソースから宛先IP(例: 別VPCのAPIサーバー)へパケットが送出されます。
2. VPCルートテーブル: 宛先IPに一致するルートとして、TGWへのルート (10.0.0.0/16 -> tgw-id) が設定されているため、パケットはVPC内のTGWアタッチメント用ENIへ転送されます。
3. TGW内部処理: パケットがTGWに到達すると、TGWは「どのVPCアタッチメントから来たか」を識別し、対応するTGWルートテーブルを参照して、次にどのVPCやVPNアタッチメントへ転送すべきかを決定します。
4. 宛先VPC: 宛先側のVPCアタッチメントを経由し、宛先サブネットのルートテーブルとセキュリティグループを通過してターゲットに到達します。

> SREの現場Tips:
> TGWアタッチメントを作成すると、指定した各アベイラビリティゾーン(AZ)のサブネット内にAWS管理のENI(Elastic Network Interface)が自動で作成されます。このENIに対して、適切なサブネット(最低でも /28 以上の空きIPが必要)を割り当てておくことが、キャパシティ枯渇を防ぐ最初の関門です。

—

3. 【実践】Terraformで構築するTGWハブ&スポーク構成

百聞は一見にしかず。ここでは、Terraformを使って「ハブとなるTGW」と「2つのスポークVPC(App VPC と DB VPC)」を接続し、相互通信を制御する構成コードを記述します。

実務でそのままコピペして応用できるよう、日本語のコメントを丁寧に添えています。

# ==========================================
# 1. Transit Gateway (TGW) 本体とルートテーブルの作成
# ==========================================

resource "aws_ec2_transit_gateway" "main" {
  description                     = "Production Hub Transit Gateway"
  default_route_table_association = "disable" # デフォルトの関連付けは無効化し、手動で厳格に制御する
  default_route_table_propagation = "disable" # デフォルトの伝搬も無効化
  dns_support                     = "enable"
  vpn_ecmp_support                = "enable"

  tags = {
    Name        = "prod-tgw-hub"
    Environment = "Production"
  }
}

# TGW用のカスタムルートテーブル(全VPC共通で相互通信を許可する例)
resource "aws_ec2_transit_gateway_route_table" "internal" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id

  tags = {
    Name = "prod-tgw-internal-rt"
  }
}

# ==========================================
# 2. スポークVPCの作成 (App VPC と DB VPC)
# ==========================================

# アプリケーション用VPC
resource "aws_vpc" "app" {
  cidr_block           = "10.100.0.0/16"
  enable_dns_hostnames = true
  tags                 = { Name = "app-vpc" }
}

# データベース用VPC
resource "aws_vpc" "db" {
  cidr_block           = "10.200.0.0/16"
  enable_dns_hostnames = true
  tags                 = { Name = "db-vpc" }
}

# ==========================================
# 3. TGWアタッチメントの作成
# ==========================================

# App VPC用サブネット(TGWアタッチメント専用サブネットを各AZに配置)
resource "aws_subnet" "app_tgw" {
  vpc_id            = aws_vpc.app.id
  cidr_block        = "10.100.100.0/28"
  availability_zone = "ap-northeast-1a"
  tags              = { Name = "app-subnet-tgw" }
}

resource "aws_ec2_transit_gateway_vpc_attachment" "app" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.app.id
  subnet_ids         = [aws_subnet.app_tgw.id]

  tags = { Name = "app-vpc-attachment" }
}

# DB VPC用サブネット
resource "aws_subnet" "db_tgw" {
  vpc_id            = aws_vpc.db.id
  cidr_block        = "10.200.100.0/28"
  availability_zone = "ap-northeast-1a"
  tags              = { Name = "db-subnet-tgw" }
}

resource "aws_ec2_transit_gateway_vpc_attachment" "db" {
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.db.id
  subnet_ids         = [aws_subnet.db_tgw.id]

  tags = { Name = "db-vpc-attachment" }
}

# ==========================================
# 4. アタッチメントのTGWルートテーブルへの関連付けと伝搬
# ==========================================

# App VPC アタッチメントをカスタムTGWルートテーブルに関連付け
resource "aws_ec2_transit_gateway_route_table_association" "app" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.app.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.internal.id
}

# DB VPC アタッチメントも同じカスタムTGWルートテーブルに関連付け
resource "aws_ec2_transit_gateway_route_table_association" "db" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.db.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.internal.id
}

# 各VPCのCIDRをTGWルートテーブルに自動/手動で伝搬させる設定
resource "aws_ec2_transit_gateway_route_table_propagation" "app" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.app.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.internal.id
}

resource "aws_ec2_transit_gateway_route_table_propagation" "db" {
  transit_gateway_attachment_id  = aws_ec2_transit_gateway_vpc_attachment.db.id
  transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.internal.id
}

# ==========================================
# 5. 各VPCのルートテーブル設定(戻りパケットの制御含む)
# ==========================================

# App VPC側のルートテーブル設定(DB VPCへ向かうトラフィックをTGWへ流す)
resource "aws_route" "app_to_db" {
  route_table_id         = aws_vpc.app.default_route_table_id
  destination_cidr_block = "10.200.0.0/16"
  transit_gateway_id     = aws_ec2_transit_gateway.main.id
}

# DB VPC側のルートテーブル設定(App VPCへ向かうトラフィックをTGWへ流す)
resource "aws_route" "db_to_app" {
  route_table_id         = aws_vpc.db.default_route_table_id
  destination_cidr_block = "10.100.0.0/16"
  transit_gateway_id     = aws_ec2_transit_gateway.main.id
}

—

4. トラブルシューティング:パケットが届かないときの「3つのチェックポイント」

設計通りにTerraformをapplyし、「よし、これでVPC間通信ができるはずだ」とAPIリクエストや疎通確認を行ったとき、無慈悲にタイムアウトエラー (Connection timed out) が返ってくることは珍しくありません。

現場のSREが障害対応時に必ず確認する、鉄板の3ステップを伝授します。

チェック1: 各VPCのルートテーブルにおける「戻りルート」の欠落

最も多い原因がこれです。App VPCからDB VPCへの送信ルート (10.200.0.0/16 -> tgw-id) は設定していても、DB VPC側からApp VPC側へ返すための戻りルート (10.100.0.0/16 -> tgw-id) がDB VPCのルートテーブルから抜けているケースです。

  • 確認コマンド (AWS CLI):
aws ec2 describe-route-tables --route-table-ids <db-vpc-route-table-id>

チェック2: セキュリティグループ(SG)とNetwork ACLの「双方向」の穴あき

AWSのセキュリティグループはステートフルですが、通信する両端のリソース(クライアントとサーバー)の双方で許可ルールが入っているかを確認する必要があります。
さらに、TGWアタッチメントを作成したサブネットにアタッチされている Network ACL(NACL) が、一時ポート(ephemeral ports: 1024-65535)のインバウンド・アウトバウンドをブロックしていないかも見落としがちなポイントです。

チェック3: TGWルートテーブルのアタッチメント関連付け(Association)と伝搬(Propagation)の不整合

TGWのルートテーブル自体に、宛先VPCのCIDRに対する適切なルートが存在しているかを確認します。

  • 確認コマンド (AWS CLI):
aws ec2 get-transit-gateway-route-table-propagations --transit-gateway-route-table-id <tgw-rtb-id>

—

5. 現場で役立つ実用コード:Pythonによる疎通確認チェッカー

複雑なマルチVPC環境では、どのパスで通信が遮断されているかをプログラムで素早く検証できると便利です。以下に、指定したエンドポイントに対してHTTPリクエストを送り、到達性を検証するPythonスクリプトのサンプルを掲載します。

import sys
import urllib.request
import urllib.error

# 検証対象のエンドポイント(例: DB VPC内にある内部APIサーバー)
TARGET_URL = "http://10.200.10.50/healthz"
TIMEOUT_SECONDS = 5

def check_connectivity(url: str):
    print(f"[*] 接続テスト開始: {url}")
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "TGW-Connectivity-Checker/1.0"})
        with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as response:
            status_code = response.getcode()
            print(f"[SUCCESS] 接続成功! ステータスコード: {status_code}")
            if status_code == 200:
                sys.exit(0)
            else:
                sys.exit(1)
                
    except urllib.error.HTTPError as e:
        # HTTPエラー(4xx, 5xx)はサーバーに到達している証拠なので、ネットワーク経路としてはOK
        print(f"[SUCCESS (HTTP Error)] サーバーへは到達しました。ステータス: {e.code}")
        sys.exit(0)
        
    except urllib.error.URLError as e:
        # タイムアウトやホスト未達はネットワーク層・SG層でのブロックの可能性が高い
        print(f"[ERROR] 接続失敗(タイムアウトまたはルーティングエラーの可能性): {e.reason}")
        sys.exit(2)

if __name__ == "__main__":
    check_connectivity(TARGET_URL)

このスクリプトをApp VPC内の踏み台サーバーやEC2インスタンス上で実行し、終了ステータスを確認することで、CI/CDパイプラインやデプロイ後のスモークテストに組み込むことも可能です。

—

まとめ

AWS Transit Gatewayによるハブ&スポーク構成は、単なる「配線の整理」ではありません。組織の拡大スピードにインフラの複雑性が負けないようにするための、スケーラビリティの防波堤です。

  • デフォルトルートテーブルの関連付け/伝搬は無効化し、明示的にカスタムルートテーブルで制御する。
  • ルートテーブル、セキュリティグループ、NACLの「行きと戻り」を常にセットで意識する。
  • TerraformなどのIaCを活用し、属人性を排除したネットワークトポロジーを維持する。

この原則を押さえておけば、どれだけVPCの数が増えようとも、あなたを悩ませるネットワークの亡霊に怯える必要はなくなります。ぜひ、次のクラウド設計の現場で実践してみてください。

コメント

タイトルとURLをコピーしました