【実務・中級編】 パブリックサブネットにおけるエラスティックIP(EIP)の割り当てとフローログの記録仕様 – クラウドインフラと仮想化ネットワーク実践ガイド

こんにちは、SREチームのシニアエンジニアです。

クラウドインフラの現場で幾度となく夜間障害の対応をしていると、「なぜか外部APIへのリクエストが特定の送信元IPから届かないと言われた」「VPCフローログを見ているのに、パケットの送信先IPとログの数値が一致して混乱した」といったトラブルに直面します。

特に、AWSのパブリックサブネットにおけるエラスティックIP(EIP)の動的なマッピングと、VPCフローログが記録するIPアドレスの「変換前後」の仕様は、ネットワークの基礎を理解していないと完全にハマるポイントです。

今回は、パケットがAWSの境界をどう駆け巡り、なぜフローログの srcAddr と dstAddr がそのように記録されるのか、実務で役立つデバッグの知見を交えて徹底解説します。

—

1. パブリックサブネットとEIP:静的パケットマッピングの裏側

まずは、EC2インスタンスにEIPを割り当てたとき、AWSの仮想化レイヤー(Nitroシステムなど)で何が起きているのかを整理しましょう。

プライベートIPアドレス(例: 10.0.1.100)を持つEC2インスタンスにEIP(例: 203.0.113.50)を割り当てると、AWSのソフトウェア定義ネットワーク(SDN)コントローラーは、ホストOSのハイパーバイザー上で1対1の双方向NAT(Network Address Translation)を即座に構成します。

アウトバウンド(外向き)通信の挙動

インスタンスからインターネット上のWeb API(例: https://api.example.com)へリクエストを飛ばす際の流れです。

1. アプリケーション層・トランスポート層:インスタンス内のプロセスが、宛先IP 198.51.100.20 に対してTCPパケットを生成します。
2. カーネル・仮想インターフェイス(ENI):パケットの送信元IPは、OSが知るプライベートIP 10.0.1.100 のままENIの仮想NICを通過します。
3. AWS Nitro/VPCルーター(SNAT):VPCの境界を抜ける直前の仮想ルーターにおいて、送信元IPがEIPである 203.0.113.50 に書き換えられます(SNAT)。TCPのポート番号は原則としてそのまま維持されますが、ポート枯渇や衝突回避のために動的に書き換えられることもあります。
4. インターネットゲートウェイ(IGW):パケットはIGWを経由してパブリックインターネットへ送り出されます。

インバウンド(内向き)通信の挙動

外部からEIP宛てにパケットが飛んできた場合、この逆の変換(DNAT)が瞬時に行われます。

  • 外部からの宛先IP: 203.0.113.50(EIP)
  • 仮想ルーターでの変換(DNAT): 宛先IPをインスタンスのプライベートIP 10.0.1.100 に書き換え。
  • インスタンス到達時: 送信元は外部クライアントのIP、宛先は自身のプライベートIPとしてカーネルに受領されます。

ここで重要なのは、EC2インスタンスのOS(Linuxなど)は、自分がEIPを持っていることを意識していないという点です。OSのネットワークインターフェイス(eth0 など)に設定されているのはあくまでプライベートIPのみです。この仕様を忘れていると、OS上のファイアウォール(iptables や nftables)やローカルバインドの設定で迷子になります。

—

2. VPCフローログの記録仕様:変換前後のトラップ

トラブルシューティングで最も多くのエンジニアを悩ませるのが、VPCフローログの仕様です。VPCフローログは、ENIを通過するIPトラフィックのメタデータを記録しますが、「どのレイヤーのENIでキャプチャするか」によって、ログに記録される srcAddr や dstAddr の値が劇的に変わります。

フローログの基本フォーマット(デフォルト)

標準的なVPCフローログのフィールド並びは以下の通りです。

version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status

EIP利用時におけるログの記録仕様(超重要)

パブリックサブネット上のEC2インスタンス(プライベートIP: 10.0.1.100、EIP: 203.0.113.50)から、外部の宛先(198.51.100.20)へ通信した際のフローログを考えてみます。

  • 送信(アウトバウンド)の場合
  • ENIで観測される値: フローログはENIの「境界(ネットワークレイヤー)」で記録されます。
  • 記録される srcAddr: 10.0.1.100(変換前のプライベートIP)
  • 記録される dstAddr: 198.51.100.20(外部宛先IP)

> 💡シニアの現場Tips:
> 「EIP(203.0.113.50)から通信しているはずなのに、VPCフローログで srcAddr をEIPで検索してもヒットしない!」という現象が頻発します。VPCフローログはENIの直近(OSに近い側)でキャプチャされるため、SNAT変換される前のプライベートIPが記録される仕様になっています。EIPベースでトラフィックを追いたい場合は、NATゲートウェイのフローログ、あるいはCloudWatch Logs Insights等でプライベートIPとEIPの紐付けを把握しておく必要があります。

—

3. 実践:Web APIクライアントの実装と検証環境の構築

では、この挙動を実務のコードとインフラ設定で確認してみましょう。今回はPythonの requests ライブラリを用いて、外部のグローバルIP確認用APIを叩くスクリプトを用意しました。

サンプルコード: 自身のグローバルIPを確認するPythonスクリプト

パブリックサブネット上のEC2にSSHまたはAWS Systems Manager (SSM)で接続し、以下のスクリプトを実行します。これにより、外部から見た自分のIPが正しくEIPに変換されていることが確認できます。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests
import sys

def check_outbound_ip():
    # 外部から見えるグローバルIPを返すパブリックなAPIサービス
    api_url = "https://httpbin.org/ip"
    
    try:
        print(f"[{api_url}] へリクエストを送信中...")
        # タイムアウトを5秒に設定し、ブロッキングを防ぐ
        response = requests.get(api_url, timeout=5.0)
        
        # HTTPステータスコードのチェック
        response.raise_for_status()
        
        data = response.json()
        origin_ip = data.get("origin", "不明")
        
        print("\n--- 通信結果 ---")
        print(f"外部から観測された送信元IP (EIP): {origin_ip}")
        print("----------------\n")
        
    except requests.exceptions.RequestException as e:
        print(f"エラーが発生しました: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    check_outbound_ip()

TerraformによるパブリックサブネットとEIPの宣言的定義

インフラストラクチャ・アズ・コード(IaC)として、この挙動を支えるネットワーク基盤をTerraformで美しく構築するスニペットも共有しておきます。

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

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

# パブリックサブネットの作成
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true # 自動パブリックIP付与を有効化(必要に応じて)

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

# ルートテーブルの設定(インターネットへの経路確保)
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

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

# サブネットとルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# エラスティックIP(EIP)の確保
resource "aws_eip" "app_server" {
  domain = "vpc"

  tags = {
    Name = "production-app-eip"
  }
}

# EC2インスタンスへのEIPの割り当て(関連付け)
resource "aws_eip_association" "app_server" {
  instance_id   = aws_instance.app.id
  allocation_id = aws_eip.app_server.id
}

—

4. 現場のトラブルシューティング・チェックリスト

最後に、実務でネットワークやEIP、フローログ関連の障害に直面した際、シニアが真っ直ぐに確認するチェックリストを授けます。

1. 「外部APIに弾かれる」と言われたら

  • 相手側のファイアウォールやホワイトリストに登録されているIPが、ENIに割り当てた「プライベートIP」になっていないか?(必ず割り当てたEIPを先方に伝えること)

2. 「VPCフローログでトラフィックが見つからない」と思ったら

  • 検索条件の srcAddr にEIPを入れていないか? ENIレベルのログではプライベートIPが記録されているため、プライベートIPで検索し直すか、CloudWatch Logs Insightsでマッピングを確認する。

3. 「セキュリティグループ(SG)のルールが効かない気がする」と思ったら

  • セキュリティグループはステートフルですが、パブリックサブネットからのアウトバウンド通信において、OS上のルーティングが正しくIGW(またはデフォルトルート)に向いているかを ip route コマンドで必ず確認する。

クラウドのネットワークは、背後にある抽象化レイヤーの挙動(今回で言えばSNATとログのキャプチャポイント)さえ頭に焼き付けておけば、決してブラックボックスではありません。

日々のアーキテクチャ設計や、いざという時の迅速な切り分けに、ぜひこの知見を活用してください。

コメント

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