こんにちは。シニアSREの私です。
クラウドのインフラ設計をしていると、「プライベートサブネットにある踏み台サーバーやコンテナから、どうやって外部のSaaSやWeb APIに安全にアクセスさせるか」という壁に必ずぶつかります。インターネットへの扉を閉ざしつつ、外の世界と通信するための生命線――それが今回深掘りする SNAT(Source Network Address Translation) です。
教科書を開けば「送信元IPアドレスをグローバルIPに書き換える技術」と一言で片付けられますが、実際の現場では、パケットがコネクション追跡テーブル(Conntrack)の荒波をどうくぐり抜け、限られたポート枯渇の危機をいかに回避しているか、そのリアルな挙動を知らなければ本番障害で痛い目をみます。
今回は、パケットの旅路を追いながら、AWS/GCPのNATゲートウェイが裏側でやっていること、そして実務で役立つデバッグやコードの書き方まで、泥臭い知見を交えて徹底解説します。
—
1. なぜSNATが必要なのか? RFC 1918の現実とIP枯渇問題
私たちが普段、プライベートサブネットで自由に使い倒している 10.0.0.0/8 や 192.168.0.0/16 といったIPアドレス。これらは [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) で定められたプライベートIPアドレスであり、パブリックなインターネットの世界ではルーティングされません。
もし、プライベートサブネットにあるKubernetesのPodやEC2インスタンスから、そのまま外部のWeb APIへパケットを送り出したとしたらどうなるでしょうか? 送信元のプライベートIPアドレス(例:10.0.1.50)を抱えたパケットは、インターネットの境界ルーターに到達した瞬間に「お前は誰だ?」とばかりにドロップされてしまいます。
そこで登場するのが SNAT です。
パケットがプライベートサブネットからインターネットへ出ていく境界(AWSのNAT GatewayやGCPのCloud NATなど)で、パケットの送信元IPアドレス(Source IP)を、クラウド側がプロビジョニングしたパブリックIPアドレス(例:203.0.113.15)に強制的に書き換えるのです。
[プライベートPod (10.0.1.50)]
│
▼ (内部パケット送信)
[NAT Gateway (送信元IPを 203.0.113.15 に書き換え)]
│
▼ (インターネットへルーティング)
[外部Web APIサーバー]
これにより、外部のサーバーから見れば「203.0.113.15 というちゃんとしたパブリックIPを持つクライアントからリクエストが来た」ように見え、無事にレスポンスを受け取ることができるようになります。
—
2. パケットの裏側:コネクション追跡(Conntrack)とポート変換の魔法
「IPアドレスを1つのパブリックIPに書き換えるだけで、複数のコンテナからの同時通信をどうやってさばいているの?」という疑問を持つ方は優秀です。プライベートサブネットには何百、何千というPodやインスタンスが存在し、それぞれが外部に大量のリクエストを投げています。
ここで鍵を握るのが、レイヤー4のポート番号(Source Port)です。
NATゲートウェイは単にIPアドレスを書き換えているだけではありません。内部で NAPT(Network Address and Port Translation / ポートアドレス変換) または PAT と呼ばれる仕組みを使い、パケットの「送信元IP+送信元ポート」の組み合わせを、パブリックIPの「空いているポート」に動的にマッピングしています。
通信のライフサイクル(シーケンス)
1. リクエスト送信:
プライベートサブネットのコンテナ(10.0.1.50:54321)が、外部API(198.51.100.10:443)へ向けたTCPパケットを送信します。
2. SNAT処理(NAT Gateway):
NAT Gatewayは、自身のステートテーブル(Conntrackテーブル)にこの通信を記録します。
- 変換前:
10.0.1.50:54321→198.51.100.10:443 - 変換後:
203.0.113.15:10001→198.51.100.10:443
送信元IPをパブリックIPに、ポートもNAT側の一意なポート(10001)に書き換えてインターネットへ送出します。
3. レスポンス受信:
外部APIサーバーは、203.0.113.15:10001 宛てのレスポンスを返します。
4. 逆変換とルーティング:
NAT Gatewayはパケットを受け取ると、自身のステートテーブルを参照し、「あ、これは中の 10.0.1.50:54321 宛ての返答だな」と即座に逆変換を行い、プライベートサブネット内の元のコンテナへ届けます。
この仕組みがあるおかげで、たった1つのパブリックIPであっても、理論上「65,000ポート強」の同時コネクションを多重化してさばくことができるのです。
—
3. 現場で直面する罠:「SNATポート枯渇」の恐怖
クラウド運用で最も恐ろしいインシデントの一つが SNATポート枯渇(SNAT Port Exhaustion) です。
マイクロサービスアーキテクチャで、1つのPodから外部のマイクロサービスやサードパーティAPIへ、コネクションプールを適切に管理せずに短期間で何万回もリクエストを飛ばすとどうなるでしょうか。あっという間にNAT Gatewayの利用可能な送信元ポートが枯れ果てます。
ポートが枯渇すると、新規のTCPハンドシェイク(SYNパケット)がNAT Gatewayで破棄され、アプリケーション側で Connection timed out や EADDRNOTAVAIL といったエラーが連発します。モニタリングツールでCPUやメモリは平穏無事なのに、なぜか外部通信だけが不通になる――典型的な「見えない障害」のパターンです。
対策の基本
- HTTP Keep-Aliveの徹底: 毎回TCPコネクションを張って切断するのではなく、コネクションを維持(再利用)してポートの消費を抑える。
- クライアント側でのコネクションプールの適切な設定。
- Cloud NATやNAT Gatewayのスケールアウト設定: GCPのCloud NATであれば「追加のIPアドレスの割り当て」や「エンドポイントあたりの最小ポート数の調整」、AWSであれば複数のNAT GatewayをマルチAZで配置して負荷分散を行う。
—
4. 実務で役立つコード例と設定のポイント
ここからは、実際にアプリケーション層やインフラ層でこの仕組みを意識するためのコード例を見ていきましょう。
① Python (requests) でのコネクションプール(Session)の活用
Pythonで外部APIを叩く際、素の requests.get() をループ内で回すと、毎回新しいTCPコネクションが張られ、SNATポートを猛烈な勢いで消費します。requests.Session を使ってコネクションを維持するのがプロの作法です。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_robust_session():
"""
SNATポートの枯渇を防ぎつつ、リトライ耐性を持たせたセッションを構築する
"""
session = requests.Session()
# リトライ戦略の設定
retries = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504],
raise_on_status=False
)
# コネクションプール(プールサイズと最大オーバーフロー)を明示的に指定
# これにより同一ホストへの通信でTCPコネクションが再利用され、SNATポートの消費を抑えられる
adapter = HTTPAdapter(
pool_connections=10,
pool_maxsize=50,
max_retries=retries
)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
if __name__ == "__main__":
api_client = create_robust_session()
# 複数回のリクエストでもコネクションが再利用されるため安全
for i in range(5):
try:
response = api_client.get("https://api.example.com/v1/status", timeout=5)
print(f"Request {i+1}: Status {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Request {i+1} failed: {e}")
② Kubernetes (NetworkPolicy / Pod設定) からの視点
EKSやGKEなどのKubernetes環境において、PodがどのNAT Gatewayを経由して外部に出るかは、ルーティングテーブルとサブネットの設計に依存します。
もし特定の高負荷なバッチ処理がある場合、そのPod群を専用のプライベートサブネット(=専用のNAT Gatewayを持つサブネット)に配置し、他のWebトラフィックへの影響を隔離するアーキテクチャがよく取られます。
Kubernetesのデプロイメント定義(YAML)で、特定のノードグループ(専用サブネットに所属するノード)にPodを誘導するための nodeSelector の例です。
apiVersion: apps/v1
kind: Deployment
metadata:
name: heavy-batch-processor
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: batch-processor
template:
metadata:
labels:
app: batch-processor
spec:
containers:
- name: processor
image: 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/batch:v1.2.0
env:
- name: API_ENDPOINT
value: "https://api.external-partner.com/v1"
# 【重要】外部通信の激しいバッチ用Podを、専用のNAT Gatewayを持つノードプールに強制配置する
nodeSelector:
node.kubernetes.io/purpose: dedicated-nat-subnet
—
5. トラブルシューティング:現場で使えるデバッグ手順
最後に、本番環境で「プライベートサブネットから外部APIへの通信が急に応答しなくなった!」という修羅場に直面した時の、シニア流の切り分け手順を伝授します。
1. DNSの名前解決ができるか確認する
まずはコンテナ内に入り、nslookup や dig で外部ホスト名が引けるか確認します(プライベートサブネットの場合、DNS/NATインスタンスやVPC DNSの不具合の可能性もあるため)。
kubectl exec -it <pod-name> -- nslookup api.external-partner.com
2. curlでパケットの疎通とレスポンスタイムを確認する
タイムアウトしているのか、接続拒否(Connection Refused)なのかを見極めます。
kubectl exec -it <pod-name> -- curl -Iv https://api.external-partner.com/v1/status
3. メトリクスでSNATポート枯渇やパケットドロップを疑う
- AWSの場合:CloudWatchの
PacketsDropCountやConnectionTimedOut、ErrorPortAllocationメトリクスをチェック。 - GCPの場合:Cloud Monitoringの
nat/port_allocation_failuresメトリクスをチェック。ここで値が跳ね上がっていれば、明確にSNATポートが枯渇しています。
4. tcpdumpでパケットの往来をキャプチャする
どうしても原因が分からない最終手段として、ネットワークインターフェースを流れるパケットを覗き見ます(権限が必要ですが確実です)。
# 例:特定の外部IP宛てのSYNパケットが出ていっているか確認
tcpdump -nnvvS -i eth0 host 198.51.100.10
—
まとめ
パブリックサブネットとプライベートサブネット、そしてその架け橋となるSNAT(NAT Gateway)。
普段は意識することの少ない裏方の仕組みですが、ひとたびシステムがスケールし、トラフィックが爆発したときには、インフラエンジニアの設計センスがモロに出るクリティカルなポイントです。
「なぜこの設計が必要なのか」「パケットはどこでどう変換されているのか」をレイヤー4のレイヤーまで解像度高くイメージできるようになれば、クラウドネットワークのトラブルなど恐れるに足りません。
皆さんのインフラが、今日も安定したパケットの奔流に包まれますように。それでは、次の現場でお会いしましょう!
コメント