【実務・中級編】 VPC(Virtual Private Cloud)の基本概念と論理分離 – クラウド&コンテナネットワーク実践ガイド

クラウドネットワークの息吹を感じろ:VPCの論理分離とパケットの軌跡

こんにちは。数々の修羅場をくぐり抜け、深夜の障害対応で幾度となくルータやクラウドのコンソール画面に祈りを捧げてきたシニアSREの私だ。

Web APIの設計やモダンなマイクロサービス開発に熱中していると、どうしてもアプリケーションコードやJSONのスキーマ定義に意識が向きがちになる。しかし、どれほど洗練されたAPIを作ろうとも、それを載せる土台であるVPC(Virtual Private Cloud)の設計が歪んでいれば、システムはいずれスケールの壁にぶつかり、あるいはセキュリティの地雷を踏むことになる。

「VPCなんて、とりあえず /16 で切って、パブリックとプライベートのサブネットを作っておけばいいんでしょ?」
もし君がそう考えているなら、少し立ち止まってほしい。クラウドの裏側で、物理的なスイッチやNIC(Network Interface Card)がどのように抽象化され、何千ものテナントのパケットが混ざり合うことなくルーティングされているのか。その「論理分離」のメカニズムを肌で理解しているかどうかで、プロのインフラエンジニアと、単なる設定ツールのオペレーターの分かれ道が決まる。

今回は、VPCの基本概念と論理分離の裏側、そして実務で直面する設計の勘所を、現場のリアルな視点から徹底的に紐解いていこう。

—

1. 物理インフラの抽象化:なぜ私たちはVPCを必要とするのか

かつてのオンプレミス環境では、ネットワークの論理分離を実現するためには、物理的なスイッチを買い増し、VLAN(IEEE 802.1Q)を切り、VLAN間ルーティングのために堅牢なレイヤ3スイッチやファイアウォールをラックにマウントする必要があった。

しかし、AWSやGCPといったメガクラウドの世界では、これらすべてのハードウェアはハイパーバイザーやスマートNIC(AWSのNitro Systemなど)のレイヤで完全にソフトウェア定義(SDN: Software-Defined Networking)されている。

マルチテナント環境における「見えない壁」

クラウドプロバイダーの巨大なデータセンター内では、何万台もの物理サーバが同一の物理ネットワークに接続されている。では、なぜ隣の企業のKubernetesクラスタと、君の開発環境のパケットが混ざらないのか?

その答えが VPCによる論理分離(Isolation) だ。

クラウドの仮想ネットワークは、VXLANなどのトンネリング技術や、ホスト側の仮想スイッチ(Open vSwitch等)におけるフロー制御、そして何重ものパケットフィルタリング(セキュリティグループやネットワークACL)によって、仮想的なカプセル化とルーティングを行っている。物理的には同じ回線を流れていても、論理的には完全に切り離された「自分だけの専用ネットワーク空間」がそこに立ち現れるのだ。

—

2. CIDRとサブネット分割のリアル:設計の数式と落とし穴

VPCを設計する際、最初に直面するのがCIDR(Classless Inter-Domain Routing)ブロックの選定だ。よくある失敗が、「とりあえず一番大きい 10.0.0.0/16 にしておけば枯渇しないだろう」という安易なアプローチだ。

実務では、将来的なVPCピアリングやオンプレミスとのVPN接続(Direct Connect / Cloud Interconnect)を見据えたIPアドレス設計が不可欠となる。社内LANや他のクラウド環境とIPアドレスが重複(IPパトレーション)すると、ルーティングテーブルがループしたり、通信がルーティング不能に陥る悲劇を引き起こす。

プライベートIPアドレス空間のRFC規約

RFC 1918で定義されているプライベートIPアドレス空間を再確認しておこう。

  • 10.0.0.0/8 (大規模エンタープライズやクラウドVPCの基幹に最適)
  • 172.16.0.0/12 (中規模システムやコンテナのオーバレイネットワークで頻出)
  • 192.168.0.0/16 (小規模オフィスや検証環境向け)

実務で使えるサブネット分割の例

例えば、1つのVPC(10.100.0.0/16)をマルチAZ(可用性ゾーン)に展開し、Web、AP、DBの各レイヤを綺麗に分離する場合のサブネット設計は以下のようになる。

VPC CIDR: 10.100.0.0/16 (総IP数: 65,536)
 ├── ゾーンA パブリック (ALB用): 10.100.0.0/24 (256 IP)
 ├── ゾーンB パブリック (ALB用): 10.100.1.0/24 (256 IP)
 ├── ゾーンA プライベート (Web/AP用): 10.100.16.0/20 (4,096 IP)
 ├── ゾーンB プライベート (Web/AP用): 10.100.32.0/20 (4,096 IP)
 ├── ゾーンA データベース用: 10.100.64.0/24 (256 IP)
 └── ゾーンB データベース用: 10.100.65.0/24 (256 IP)

ここで注意してほしいのは、クラウドプロバイダー(例えばAWS)は、各サブネットの先頭4つのIPアドレスと末尾の1つのIPアドレス(合計5個)を内部予約(ゲートウェイ、DNS、将来の拡張用など)として使用するため、利用可能なIP数は実際のCIDR計算よりも少なくなっている点だ。/28 のような小さすぎるサブネットを切ると、KubernetesのPod数がスケールした瞬間にIP枯渇エラー(IPAM exhaustion)を踏むことになるので注意してほしい。

—

3. パケットの旅:インターネットからWeb APIへのルーティングフロー

では、ユーザーがクライアント端末からクラウド上のWeb APIを叩いたとき、パケットはVPC内でどのようにルーティングされるのだろうか。そのシーケンスを追ってみよう。

[Client] 
   │ (HTTPS Request: GET /api/v1/users)
   ▼
[Internet Gateway (IGW)]
   │ (VPCの境界でルータによるSNAT/DNAT処理・パケット検査)
   ▼
[Public Subnet: ALB (Application Load Balancer)]
   │ (リスナーでTLS終端し、ターゲットグループへ転送)
   ▼
[Route Table (プライベートサブネット)]
   │ (宛先 0.0.0.0/0 を NAT Gateway へ向けるルーティング)
   ▼
[Private Subnet: API Server (ECS / EKS / EC2)]
   │ (コンテナ内アプリがリクエストを受信・処理)
   ▼
[Database Subnet: RDS (PostgreSQL)]

現場のTips:インターネットゲートウェイとNATの罠

プライベートサブネットに配置されたバックエンドのAPIサーバから外部の外部API(例えばStripeやSendGridなど)を呼び出す場合、パケットは必ず NAT Gateway を経由しなければならない。

ここでよくあるトラブルが、「NAT Gatewayの帯域枯渇」や「Elastic IP(EIP)のポート枯渇(SNATポートの枯渇)」だ。数千のマイクロサービスが一斉に外部APIへリクエストを投げると、一時ポート(Ephemeral Port)が枯渇し、タイムアウトエラーが頻発する。設計段階からNAT Gatewayの配置設計や、コネクションプーリングのチューニング(HTTP Keep-Aliveの有効化など)を怠らないようにしよう。

—

4. 実装例:インフラ定義とAPIからの疎通確認

理論が分かったところで、実務に直結するコードを見ていこう。今回は、IaC(Infrastructure as Code)のデファクトであるTerraformによるVPC定義の断片と、実際にそのVPC上で稼働するAPIに対してクライアントからリクエストを送るPythonコードの例を示す。

TerraformによるVPC・サブネットの論理分離定義

以下のコードは、単一のVPC内にパブリックサブネットとプライベートサブネットを定義し、ルーティングテーブルを適切に結びつける王道のパターンだ。

# VPCの定義 (CIDRブロックを /16 で確保)
resource "aws_vpc" "production" {
  cidr_block           = "10.200.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "prod-vpc"
    Environment = "production"
  }
}

# パブリックサブネットの定義 (ALBやNAT Gateway用)
resource "aws_subnet" "public_a" {
  vpc_id            = aws_vpc.production.id
  cidr_block        = "10.200.1.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "prod-subnet-public-a"
  }
}

# プライベートサブネットの定義 (APIサーバやコンテナ用)
resource "aws_subnet" "private_a" {
  vpc_id            = aws_vpc.production.id
  cidr_block        = "10.200.16.0/20"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "prod-subnet-private-a"
  }
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.production.id

  tags = {
    Name = "prod-igw"
  }
}

# パブリックルーティングテーブル (全トラフィックをIGWへ)
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.production.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "prod-rt-public"
  }
}

# パブリックサブネットとルーティングテーブルの関連付け
resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a.id
  route_id       = aws_route_table.public.id
}

PythonによるAPIリクエストとネットワークエラーのハンドリング

VPC内のプライベートAPIエンドポイントに対して、アプリケーションからリクエストを送信する際のPython(requestsライブラリ)のサンプルコードだ。現場では、ネットワークの瞬断やDNS解決エラーに対するリトライ処理(指数バックオフ)が必須となる。

import logging
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ログの設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def create_robust_session() -> requests.Session:
    """
    ネットワークの瞬断やタイムアウトに備えたリトライ機構付きのセッションを生成する
    """
    session = requests.Session()
    
    # リトライ戦略の設定 (合計3回リトライ、バックオフ倍率は0.5秒)
    retries = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[500, 502, 503, 504],
        raise_on_status=False
    )
    
    adapter = HTTPAdapter(max_retries=retries)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    return session

def fetch_api_data(endpoint_url: str, token: str) -> dict:
    session = create_robust_session()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    try:
        # タイムアウトを接続(connect)2秒、読み取り(read)5秒に厳格に設定
        response = session.get(endpoint_url, headers=headers, timeout=(2.0, 5.0))
        
        # ステータスコードに応じたハンドリング
        if response.status_code == 200:
            logger.info("APIリクエストが正常に完了しました。")
            return response.json()
        else:
            logger.error(f"APIエラー: ステータスコード {response.status_code}, レスポンス: {response.text}")
            response.raise_for_status()
            
    except requests.exceptions.Timeout:
        logger.error("VPC内のルーティング遅延またはターゲットサーバの無応答によりタイムアウトが発生しました。")
        raise
    except requests.exceptions.ConnectionError as e:
        logger.error(f"ネットワーク接続エラー。セキュリティグループやネットワークACLを確認してください: {e}")
        raise
    except Exception as e:
        logger.error(f"予期せぬエラーが発生しました: {e}")
        raise

if __name__ == "__main__":
    # プライベートVPC内の内部ALBやAPI Gatewayののエンドポイントを想定
    API_ENDPOINT = "https://internal-api.prod.local/v1/health"
    API_TOKEN = "dummy_token_for_demonstration"
    
    # 実行例(実際のエンドポイントに合わせて書き換えてください)
    # data = fetch_api_data(API_ENDPOINT, API_TOKEN)

—

5. まとめ:トラブルシューティングの極意

ネットワークの基礎であるVPCと論理分離の概念は、一見すると地味で目立たない存在かもしれない。しかし、アプリケーションが「繋がらない」「重い」「504 Gateway Timeoutを吐く」というトラブルに見舞われたとき、最後に頼りになるのは、パケットがどのルートを通り、どのセキュリティグループで弾かれているのかを脳内で正確にトレースできるエンジニアの勘と経験だ。

現場で障害に直面したときは、慌ててコードを書き換える前に、以下のチェックリストを思い出してほしい。

1. ルーティングテーブル(Route Table)の確認: パケットは正しい宛先(IGW, NAT GW, VPC Peering等)に向かっているか?
2. セキュリティグループ(Security Group)とNACLの確認: ステートフル・ステートレスの違いを理解し、往復のトラフィックの許可ポートが空いているか?
3. DNSの解決先: 内部プライベートゾーン(Route 53 Private Hosted Zone等)の正引き・逆引きが期待通りに行われているか?

インフラとアプリの境界線を取り払い、ネットワークの鼓動を感じ取れるエンジニアを目指して、日々の設計と向き合っていこう。

コメント

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