こんにちは。現場のSREとして、日々クラウドのネットワークの底地を這い回っている私です。
Web APIの設計やインフラ運用をしていると、「このリクエスト、本当にちゃんと外のインターネットに出ていってるのか?」「返り値のIPアドレス、なんでプライベートIPじゃなくてあっちのグローバルIPに化けてるんだ?」といった疑問に直面したことはありませんか?
クラウド、特にAWSのVPC(Virtual Private Cloud)を使っていると、あたかも魔法のようにプライベートIPを持つインスタンスが外の世界と通信できてしまいます。しかし、魔法の裏側には、厳密なネットワークの物理・仮想レイヤーの泥臭い仕組みが存在します。
今回は、パブリックサブネットに配置されたElastic IP(EIP)を持つEC2インスタンスを主役に据え、インターネットゲートウェイ(IGW)を通過する際のSNAT(Source NAT)とDNAT(Destination NAT)のリアルなパケット処理プロセスを、現場の知見をたっぷり交えて紐解いていきましょう。教科書には載っていない、パケットの「心の声」が聞こえるようになるのが今回のゴールです。
—
1. そもそもAWSのVPCネットワークにおける「NAT」の正体とは?
私たちが普段何気なく使っているAWSのVPC。実は、インスタンス(ENI: Elastic Network Interface)に割り当てられているIPアドレスは、基本的にプライベートIPアドレス(RFC 1918に基づくもの等)です。
しかし、パブリックなWeb APIを叩いたり、外部のSaaSと連携したりするためには、宛先(または送信元)がインターネット上でルーティング可能なグローバルIPアドレスである必要があります。
ここで登場するのが、パブリックサブネットのルートテーブルに紐づくインターネットゲートウェイ(IGW)です。IGWは、VPCのプライベート空間とカオスなインターネットを繋ぐ境界線であり、ここでマジック(アドレス変換)が行われます。
SNAT(Source Network Address Translation)とは
インスタンスから外の世界(インターネット)へ向けてパケットを送り出す際、送信元のプライベートIPアドレスを、AWS側で保持しているグローバルIPアドレス(EIPなど)に書き換える処理です。
- なぜ必要か? プライベートIPアドレスはインターネット上でルーティングされないため、そのままでは返り値が戻ってこないからです。
DNAT(Destination Network Address Translation)とは
インターネット上のクライアントから、あなたの持つEIP宛てにパケットが飛んできた際、宛先のグローバルIPアドレスを、対応するインスタンスのプライベートIPアドレスに書き換える処理です。
- なぜ必要か? パケットがIGWに到達した時点ではグローバルIP宛てですが、VPC内のEC2インスタンスは自分のプライベートIP宛てのパケットしか処理できないためです。
—
2. 通信の全貌:パケットがたどる運命のシーケンス
それでは、パブリックサブネット内のEC2(プライベートIP: 10.0.1.100、EIP: 203.0.113.50)から、インターネット上の外部APIサーバー(IP: 198.51.100.200)へHTTPSリクエストを投げるシーンを想像してください。
通信の往復において、パケットのIPヘッダーとポート番号がどのように書き換わっているのか、シーケンスを見てみましょう。
[EC2 (10.0.1.100)]
│
│ (1. アウトバウンド通信開始)
▼
[IGW (SNAT実行)] ──(送信元IPを 203.0.113.50 に書換)──> [インターネット] ──> [外部API (198.51.100.200)]
│ │
│ (2. レスポンス返却) │
│ <──(宛先IPを 203.0.113.50 に書換)──────────┘
│
[IGW (DNAT/コネクション追跡実行)]
│ (宛先IPを 10.0.1.100 に書換)
▼
[EC2 (10.0.1.100)] に到着!
ステップ1: アウトバウンド(外向き)の通信とSNAT
1. EC2からの出立:
アプリケーションから https://198.51.100.200/api に対してリクエストが発生します。
- 送信元(Src):
10.0.1.100:54321(プライベートIP + エフェメラルポート) - 宛先(Dst):
198.51.100.200:443
2. IGWでのSNAT処理:
パケットがパブリックサブネットのルートテーブルに従いIGWに到達します。IGW(厳密にはAWSの分散型ソフトウェア定義ネットワーク(SDN)基盤)は、このインスタンスに紐づくEIP (203.0.113.50) を参照し、パケットの送信元IPアドレスを書き換えます。
- 書換後(Src):
203.0.113.50:54321 - 書換後(Dst):
198.51.100.200:443
ステップ2: インバウンド(内向き)のレスポンスとDNAT
1. 外部APIからの返答:
外部APIサーバーは、見知らぬプライベートIP (10.0.1.100) ではなく、しっかり正規のグローバルIP (203.0.113.50) に対してレスポンスを返します。
- 送信元(Src):
198.51.100.200:443 - 宛先(Dst):
203.0.113.50:54321
2. IGWでのDNATおよびコネクション追跡:
IGWに戻ってきたパケットを、AWSのネットワーク基盤がコネクションテーブル(ステートフルインスペクション)と照合します。「あ、これはさっき 10.0.1.100:54321 が外へ投げたやつの返り値だな」と即座に特定し、宛先IPアドレスを元のプライベートIPに書き戻します。
- 書換後(Src):
198.51.100.200:443 - 書換後(Dst):
10.0.1.100:54321
3. EC2への到着:
無事にEC2のOSカーネル(netfilter / iptables等)がパケットを受け取り、アプリケーション層へデータを渡します。
—
3. 実務で役立つ!コードとデバッグの現場Tips
理屈が分かったところで、これを実務のコードや運用の現場でどう活かすかを見ていきましょう。今回はPythonの requests ライブラリを用いて、実際に外部APIを叩くコードと、ネットワークの挙動を追うためのデバッグ手法をご紹介します。
実装例:Pythonによる外部APIリクエスト
以下のコードは、パブリックサブネット上のEC2から外部のIP確認APIを叩き、自分自身がインターネットからどう見えているかを検証するスクリプトです。
import sys
import requests
def check_outbound_ip():
# 接続先のパブリックAPI(グローバルIPをそのまま返すサービス)
target_url = "https://httpbin.org/ip"
try:
# タイムアウトを3秒に設定(クラウド間通信の鉄則)
response = requests.get(target_url, timeout=3.0)
# ステータスコードのチェック
response.raise_for_status()
data = response.json()
print(f"[INFO] 外部から見えている送信元グローバルIP: {data.get('origin')}")
except requests.exceptions.RequestException as e:
print(f"[ERROR] ネットワーク通信に失敗しました: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
check_outbound_ip()
このスクリプトを実行した際、出力される origin のIPアドレスが、そのEC2インスタンスにアタッチされているElastic IPの数値と完全に一致していれば、SNATが正常に機能している証拠です。もしNATゲートウェイやプロキシを経由している場合は、別の共有IPが表示されるため、設計通りのルーティングになっているかの簡易チェックにも使えます。
—
トラブルシューティング:パケットが届かない時の「泥臭い」デバッグ手順
インフラ運用で最も肝を冷やすのが、「なぜか外向きの通信がタイムアウトする」というインシデントです。綺麗なアーキテクチャ図を描いても、現実は容赦なくパケットをドロップします。そんな時に私が現場で必ず実行するステップを伝授します。
1. セキュリティグループとネットワークACL(NACL)の二重チェック
- 落とし穴: 「セキュリティグループ(SG)でアウトバウンドを
ALLにしたから大丈夫」と思っていませんか? - 確認ポイント: NACLはステートレスです。パブリックサブネットのNACLで、インバウンドのエフェメラルポート(TCP
1024-65535)の戻りパケットが拒否されていないか、あるいはアウトバウンドのポート443がブロックされていないかを明示的に確認してください。
2. ルートテーブルの次ホップ(Target)の確認
- 落とし穴: パブリックサブネットと言いながら、ルートテーブルに
0.0.0.0/0の宛先がigx-xxxxxxxx(IGW)ではなく、NATゲートウェイやローカルに向いている。 - 確認ポイント: AWS CLIでサクッとルーティングを確認します。
# 特定のルートテーブルのルーティング情報を確認するAWS CLIコマンド
aws ec2 describe-route-tables \
--route-table-ids rtb-0123456789abcdef0 \
--query "RouteTables[].Routes[*].{Destination:DestinationCidrBlock, Target:GatewayId}" \
--output table
3. EC2内部からのパケットキャプチャ(tcpdump)
もしOSレベルまでパケットが来ているのか、あるいは外へ出ようとして消えているのかを切り分けたい場合は、EC2にSSHでログインして tcpdump を回します。
# 外部APIのIP宛ての通信をキャプチャし、パケットが外に向かって流れているか確認する
sudo tcpdump -nnvvS -i eth0 host 198.51.100.200
ここでパケットの送信元IPがきちんとプライベートIP (10.0.1.100) になっているか、そしてTCPの3ウェイハンドシェイク(SYN -> SYN-ACK -> ACK)がどこで途切れているかを確認すれば、問題がAWS側のネットワークにあるのか、OSやアプリケーションにあるのかが秒速で判明します。
—
まとめ
今回は、AWSのパブリックサブネットにおけるインターネットゲートウェイを介したSNAT・DNATの裏側の動きを、パケットの視点から深掘りしました。
- SNAT: プライベート空間の安全性を保ったまま、IGWが送信元IPをEIPへと魔法のように書き換えて外へ送り出す。
- DNAT: 返ってきたパケットをステートフルに追跡し、元のEC2のプライベートIPへと正確に連れ戻す。
クラウドがどれだけ抽象化されて便利になっても、その下を流れているのは泥臭いIPパケットの群れです。この仕組みを解像度高く理解しているかどうかが、複雑なマイクロサービス間通信や突発的なネットワーク障害を切り抜けるときの強力な武器になります。
皆さんの日々のインフラ運用やAPI設計に、今回の知見が少しでも役立てば幸いです。それでは、また次回の現場でお会いしましょう!
コメント