【実務・中級編】 VPCピアリング(VPC Peering)の確立とルーティング設計 – クラウド&コンテナネットワーク実践ガイド

VPCピアリングの罠と真実:非推移的ルーティングの壁を越え、確実なマルチVPC通信をデザインする

こんにちは。AWSやGCPのインフラ構築からKubernetesのネットワークの底地まで、幾多のパケットロスやルーティング迷子と格闘してきたシニアSREの私です。

Web APIのマイクロサービス化や、セキュリティ境界(PCI DSSや機微情報管理など)に基づくVPCの分割が進むにつれて避けて通れないのが、「VPC間通信」の設計です。その中でも最も基本的かつ、いまだに多くの現場で設定ミスを引き起こすのが「VPCピアリング(VPC Peering)」です。

「とりあえずコンソールで接続ボタンを押して、ルートテーブルに相手のCIDRを追加すれば繋がるんでしょ?」

そう思って手を動かした結果、APIがタイムアウトし、CloudWatch LogsやVPC Flow Logsを深夜に睨めっこするハメになった若手エンジニアを何人も見てきました。VPCピアリングはシンプルに見えて、「非推移的(Non-transitive)である」という鉄の掟や、両側のルートテーブルの設定漏れなど、ハマりどころが満載です。

今回は、パケットがVPCの境界をどう越えていくのか、そのリアルな挙動と、実務で絶対に失敗しないための設計・デバッグ手法を、コードや設定例を交えて徹底的に解説します。

—

1. VPCピアリングの基本仕様と「非推移的ルーティング」の呪縛

まず、AWSのVPCピアリング(GCPであればVPCネットワークピアリング)の根幹を成す仕様を整理しましょう。

VPCピアリングは、2つのVPC間でプライベートIPアドレス(RFC 1918やグローバルIP)を使用した直接的な通信を可能にするネットワーキング機能です。インターネットゲートウェイやNATゲートウェイ、VPNなどを経由せず、クラウドプロバイダのバックボーンネットワーク上をパケットが直接流れるため、低レイテンシかつセキュアです。

しかし、ここで最も重要な、そして初心者が必ずハマる制約があります。それが「非推移的(Non-transitive)ルーティング」です。

非推移性とは何か?

言葉を換えて説明しましょう。「VPC A」と「VPC B」がピアリングされており、さらに「VPC B」と「VPC C」がピアリングされているとします。このとき、「VPC A」から「VPC C」へ、VPC Bを中継して通信することはできません。

[VPC A] <--- ピアリング ---> [VPC B] <--- ピアリング ---> [VPC C]
  (AからCへは直接通信できない!Bを中継したルーティングは不可)

もしVPC AからVPC Cへ通信したいのであれば、AとCの間にもう一本、直接VPCピアリングを張る必要があります。この制約を無視して、「Bがルーターの代わりをしてくれるだろう」とルートテーブルを設定しても、AWS(あるいはGCP)のルーターはVPC B宛てではないパケット(宛先がVPC CのIP)を容赦なくドロップします。

重複CIDRブロックの禁止

もう一つの絶対的な制約が、ピアリングを組む両者の間でCIDRブロックが重複していてはならないという点です。例えば、VPC Aが 10.0.0.0/16 で、VPC Bも 10.0.0.0/16 を使っている場合、ピアリングは確立できません。社内ニートワークや他社システムを統合する際によくある失敗ですので、IPアドレス設計の段階でCIDRのゾーニング(例: 10.100.0.0/16 と 10.200.0.0/16 など)を厳格に行う必要があります。

—

2. 通信確立のための双方向ルートテーブル設定要件

VPCピアリングが「確立(Active)」状態になったからといって、すぐに通信ができるわけではありません。ここが初心者の罠です。ピアリング接続はあくまで「トンネルの物理工事が終わった状態」に過ぎず、そこを通るパケットの「道案内(ルート)」は、両側のVPCのルートテーブルに人間が明示的に書いてやる必要があります。

通信が成立するためには、往復のルート(Requestの行きとResponseの帰り)の双方が完璧に設定されている必要があります。

具体的なルーティングの例

  • VPC A (CIDR: 10.1.0.0/16) 内のプライベートサブネットから、
  • VPC B (CIDR: 10.2.0.0/16) 内のWeb APIサーバーにアクセスする場合

1. VPC A側のルートテーブル設定

VPC A側のサブネットに関連付けられたルートテーブルに、以下を追加します。

  • 宛先 (Destination): 10.2.0.0/16 (VPC BのCIDR)
  • ターゲット (Target): pcx-xxxxxxxxxxxxxxxxx (VPCピアリングID)

2. VPC B側のルートテーブル設定(★ここが忘れがち!)

VPC BのAPIサーバーがリクエストを受け取り、「返事(レスポンス)」をVPC Aに返すためのルートが必要です。VPC B側のサブネットに関連付けられたルートテーブルにも設定を追加します。

  • 宛先 (Destination): 10.1.0.0/16 (VPC AのCIDR)
  • ターゲット (Target): pcx-xxxxxxxxxxxxxxxxx (同じピアリングIDを指定)

この「片道切符」状態(VPC A側だけにルートがあって、B側がない)のとき、パケットは往きますが帰ってこれず、クライアント側からは「接続がタイムアウトしました (Connection timed out)」という最も絶望的なエラーとして観測されます。

—

3. 実務で役立つ設定例:Terraformによる堅牢なVPCピアリング構築

現場のインフラ構築で手動コンソール操作などナンセンスです。Infrastructure as Code(IaC)、ここではTerraformを使って、VPC AとVPC Bのピアリングおよび双方向のルーティングを美しくコード化してみましょう。

# ==========================================
# 1. VPC A の定義
# ==========================================
resource "aws_vpc" "vpc_a" {
  cidr_block           = "10.1.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "production-vpc-a"
  }
}

resource "aws_subnet" "subnet_a" {
  vpc_id            = aws_vpc.vpc_a.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "production-subnet-a"
  }
}

# ==========================================
# 2. VPC B の定義
# ==========================================
resource "aws_vpc" "vpc_b" {
  cidr_block           = "10.2.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "production-vpc-b"
  }
}

resource "aws_subnet" "subnet_b" {
  vpc_id            = aws_vpc.vpc_b.id
  cidr_block        = "10.2.1.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "production-subnet-b"
  }
}

# ==========================================
# 3. VPCピアリング接続の作成 (VPC AからVPC Bへリクエスト)
# ==========================================
resource "aws_vpc_peering_connection" "peer_ab" {
  vpc_id      = aws_vpc.vpc_a.id   # リクエ側のVPC
  peer_vpc_id = aws_vpc.vpc_b.id   # アクセス先のVPC
  auto_accept = true               # 同一アカウント内であれば自動承認

  tags = {
    Name = "vpc-a-to-vpc-b-peering"
  }
}

# ==========================================
# 4. VPC A 側のルートテーブル設定(往路)
# ==========================================
resource "aws_route_table" "rt_a" {
  vpc_id = aws_vpc.vpc_a.id

  route {
    cidr_block = "10.2.0.0/16" # VPC B宛てのパケットをピアリングに向ける
    vpc_peering_connection_id = aws_vpc_peering_connection.peer_ab.id
  }

  tags = {
    Name = "route-table-vpc-a"
  }
}

resource "aws_route_table_association" "rta_a" {
  subnet_id      = aws_subnet.subnet_a.id
  route_table_id = aws_route_table.rt_a.id
}

# ==========================================
# 5. VPC B 側のルートテーブル設定(復路)
# ==========================================
resource "aws_route_table" "rt_b" {
  vpc_id = aws_vpc.vpc_b.id

  route {
    cidr_block = "10.1.0.0/16" # VPC A宛てのパケット(レスポンス)をピアリングに向ける
    vpc_peering_connection_id = aws_vpc_peering_connection.peer_ab.id
  }

  tags = {
    Name = "route-table-vpc-b"
  }
}

resource "aws_route_table_association" "rta_b" {
  subnet_id      = aws_subnet.subnet_b.id
  route_table_id = aws_route_table.rt_b.id
}

このコードのポイントは、enable_dns_hostnames と enable_dns_support を明示的に有効にしている点です。VPCピアリング越しにAWS内部のプライベートDNS名(例: RDSのエンドポイントなど)を名前解決させたい場合、この設定が抜けていると名前解決でコケます。

—

4. API通信の検証とデバッグTips:パケットはどこで消えたか?

構築が終わったら、VPC A内の踏み台サーバーやコンテナから、VPC B内のWeb APIに対して疎通確認を行います。

例えば、Pythonの requests や urllib を使って、VPC B側のプライベートIPを持つAPIサーバーを叩くコードは以下のようになります。

import requests
import sys

# VPC BにデプロイされたAPIサーバーのエンドポイント(プライベートIP)
API_URL = "http://10.2.1.50:8080/health"

def check_api_connection():
    try:
        print(f"Connecting to {API_URL}...")
        # タイムアウトを3秒に設定し、ハングアップを防ぐ
        response = requests.get(API_URL, timeout=3)
        
        if response.status_code == 200:
            print("[SUCCESS] VPC Peering connection is healthy!")
            print(f"Response Body: {response.text}")
        else:
            print(f"[WARNING] API responded with status code: {response.status_code}")
            
    except requests.exceptions.ConnectTimeout:
        print("[ERROR] Connection timed out! Check Route Tables, Security Groups, and NACLs.", file=sys.stderr)
    except requests.exceptions.ConnectionError as e:
        print(f"[ERROR] Connection failed: {e}", file=sys.stderr)

if __name__ == "__main__":
    check_api_connection()

それでも繋がらないときの「現場のデバッグチェックリスト」

もし上記のスクリプトが [ERROR] Connection timed out を吐き続けた場合、以下の順番でインフラを疑ってください。パケットの旅を上流から下流へ追跡します。

1. セキュリティグループ(Security Groups)の穴あき確認

  • VPC B側のAPIサーバーのセキュリティグループは、VPC AのCIDR (10.1.0.0/16) からの TCP/8080 インバウンドを許可していますか?(自分自身のセキュリティグループIDを指定しているトラップによく引っかかります)。
  • VPC A側のクライアントのセキュリティグループはアウトバウンドを塞いでいませんか?(デフォルトは全許可ですが、制限している場合があります)。

2. ネットワークACL(NACL)の確認

  • サブネットレベルのファイアウォールであるNACLで、往復ともにトラフィックがブロックされていないか確認します。NACLはステートレス(往復両方のルールが必要)なので、エフェメラルポート(高位ポート)の戻りトラフィックを忘れていないか要チェックです。

3. DNS解決の罠

  • コード内でIPアドレスではなくドメイン名(例: api.vpc-b.internal)を指定している場合、VPC Aから名前解決したIPが、VPC BのプライベートIPを正しく指しているか、かつ先述の enable_dns_hostnames が有効になっているかを確認してください。

4. VPC Flow Logsによる答え合わせ

  • どうしても原因が分からないときは、該当サブネットのVPC Flow Logsを有効化し、REJECT されているパケットの dstaddr や srcaddr をCloudWatch Logsで検索します。パケットがどのセキュリティグループやルートで拒絶されたかが srcAction や dstAction で一目瞭然になります。

—

まとめ

VPCピアリングはクラウドネットワークの基礎中の基礎ですが、だからこそ「分かっているつもり」が一番の事故を引き起こします。

  • 非推移性:中継はできない。必要なら直接繋ぐ。
  • 双方向ルート:往きがあれば必ず帰り(復路)のルートを書く。
  • DNSとセキュリティ:名前解決の設定と、セキュリティグループの相互許可を忘れない。

この3点を頭に叩き込んでおけば、どんなに複雑なマルチVPC環境の設計でも、迷うことなく堅牢なネットワークを組み上げることができるはずです。皆さんのシステムからパケットロスが撲滅されることを祈っています。それでは、良きSREライフを!

コメント

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