【実務・中級編】 プライベートサブネットからのアウトバウンド通信における送信元ネットワークアドレス変換(SNAT)の内部挙動 – クラウド&コンテナネットワーク実践ガイド

【SREが徹底解説】パケットは裏でどう化けているのか?プライベートサブネットからのSNATとポート枯渇のリアル

おい、調子はどうだい?
先日、社内の若手エンジニアからこんな切実な質問を受けたんだ。

「先輩、プライベートサブネットに置いたECSタスクから外部の決済APIを叩いてるんですけど、急にタイムアウト頻発するようになったんです。セキュリティグループもルートテーブルも完璧なはずなのに、なんでですかね……?」

こういうトラブルシューティング、君も一度は経験があるんじゃないかな?
画面上の設定は正しい。でも、パケットの気持ちになってその裏側の挙動を追ってみると、AWSやGCPのマネージドなNATゲートウェイの「見えない制約」が牙を剥いていた、なんてことはインフラ現場では日常茶飯事だ。

教科書には「プライベートインスタンスはNATゲートウェイ経由で外に出られます」としか書いていない。だが、シニアなSREとしてメシを食っていくなら、その裏でパケットの送信元IPアドレスとポートがどう書き換えられ、コネクション管理がどう行われているのか、そのミクロな挙動まで解像度高く理解しておく必要がある。

今回は、プライベートサブネットからのアウトバウンド通信における、SNAT(Source Network Address Translation)の内部挙動とポート割り当ての仕組みについて、実務に直結する知識を総ざらいしていこう。

—

1. なぜSNATが必要なのか? RFC 1918の宿命とパケットの変身劇

まず、基本のおさらいからだ。私たちがクラウド上に構築するVPC内のプライベートサブネット(AWSなら10.0.0.0/16や172.16.0.0/12など)で使われているIPアドレスの大部分は、RFC 1918で定められた「プライベートIPアドレス」だ。これらはインターネット上でルーティングされない。つまり、宛先から返りパケットを受け取るためには、グローバルIPアドレスに変換してやる必要がある。

ここで登場するのが、AWSのNATゲートウェイやGCPのCloud NATといったマネージドなNATデバイスだ。

プライベートサブネットにあるインスタンス(例えば10.0.1.50)が、外部のWeb API(例: 203.0.113.50:443)へHTTPSリクエストを投げたとしよう。この時、パケットのライフサイクルは次のようなドラマチックな変身を遂げる。

1. プライベートインスタンスの送出

  • 送信元: 10.0.1.50:54321
  • 宛先: 203.0.113.50:443
  • インスタンスから出たパケットは、VPCのルートテーブルに従ってNATゲートウェイへ向けてルーティングされる。

2. NATゲートウェイでのSNAT(アドレス・ポート変換)

  • 送信元: 198.51.100.10:49152 (※NATゲートウェイのパブリックIPと、割り当てられたエフェメラルポート)
  • 宛先: 203.0.113.50:443
  • NATゲートウェイは、自身のコネクション追跡テーブル(Conntrack)にこのマッピングを記録し、送信元のIPとポートを書き換えてインターネットへ送り出す。

3. 外部APIサーバーからの応答

  • 外部サーバーから見ると、通信の相手はプライベートインスタンスではなく、NATゲートウェイのパブリックIP(198.51.100.10:49152)だ。サーバーはそのまま応答を返す。

4. NATゲートウェイでの逆変換(DNAT)

  • 応答パケットがNATゲートウェイに戻ってきたら、デバイスはConntrackを参照し、「あ、これはさっきの10.0.1.50:54321宛ての返りだな」と特定してIPとポートを元に戻し、プライベートインスタンスへ届けむ。

これがSNATの基本だ。だが、現場で問題になるのは「IPアドレスの変換」そのものではなく、「ポートの割り当てと枯渇」なのだ。

—

2. 恐るべき「ポート枯渇」のメカニズム

TCP/IPの通信において、一意のコネクションを識別するのは「送信元IP」「送信元ポート」「宛先IP」「宛先ポート」の4要素(4タプル)だ。

ここで注意してほしい。AWSのNATゲートウェイの1つのパブリックIPアドレスあたり、利用できる送信元ポート(エフェメラルポート)の数は、OSの制限も含めて理論上最大で約64,000個(実際にはシステム予約や管理用を除き、概ね約60,000ポート程度)だ。

もし、君のシステムが次のような状態だったらどうなるだろう?

  • 大量の非同期ワーカーやコンテナ(例: 数百台のECSタスク)が稼働している。
  • 外部の特定サードパーティAPIへ、毎秒数千件のリクエストを爆撃している。

ここで、NATゲートウェイから同じ外部IP/ポートへの同時コネクション数が増えると、NATゲートウェイ側で利用できる送信元ポートが瞬く間に食い潰される。これがSNATポート枯渇(Port Exhaustion)だ。

現場でこれが発生すると、アプリケーション側では次のようなエラーが牙をむく。

# Pythonのrequestsやurllib3で外部APIを叩いている際によく見る絶望的なエラー
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.example.com', port=443): Max retries exceeded with url: /v1/data (Caused by NewupError('<urllib3.connection.HTTPSConnection object at 0x7f9a...>: Failed to establish a new establishment: Connection timed out'))

「あれ?相手のサーバーは落ちてないぞ?」と思ったら、自分たちの出口(NATゲートウェイ)でポートの空き待ち(バックプレッシャー)が発生し、パケットがドロップまたはタイムアウトしていた、というオチだ。

—

3. 実務で役立つ! コネクション効率化とコード実装のポイント

この泥沼にハマらないために、SREとして私たちが仕込んでおくべき設計・実装のプラクティスをいくつか伝授しよう。

① コネクションプールの徹底(Keep-Aliveの活用)

毎回新しいTCPコネクションを張って(3-way handshake)、リクエストを投げて、即座に切断(FIN/ACK)しているようなコードは、SNATポートをドブに捨てるようなものだ。TCPのTIME_WAIT状態も含めて、ポートが一定時間占有されてしまう。

HTTPクライアントを使うときは、必ずKeep-Alive(コネクションプールの再利用)を有効にすること。

Python(requestsライブラリ)を例に取ってみよう。

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

def create_robust_session():
    """
    SNATポート枯渇を防ぐため、コネクションプールとリトライ機構を備えた
    Requests Sessionを生成するサンプル
    """
    session = requests.Session()

    # リトライ戦略の設定(一時的なネットワークエラーやレートリミット対策)
    retries = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[500, 502, 503, 504],
        raise_on_status=False
    )

    # アダプターを設定し、プールサイズを明示的に確保
    # pool_connections: プールの数, pool_maxsize: 各プール内の最大同時接続数
    adapter = HTTPAdapter(
        pool_connections=50,
        pool_maxsize=50,
        max_retries=retries
    )

    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    return session

# グローバルまたはシングルトンとしてセッションを保持し、使い回す
api_client = create_robust_session()

def call_external_api():
    try:
        # 同一のSessionオブジェクトを使い回すことで、TCPコネクションが再利用(Keep-Alive)され、
        # 新規のSNATポート消費を劇的に抑えられます。
        response = api_client.get("https://api.example.com/v1/data", timeout=5.0)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"API呼び出し失敗: {e}")
        return None

Node.js(Fetch APIやaxios)の場合でも、Agentの設定でKeep-Aliveを有効にするのが鉄則だ。

// Node.jsのaxios等でKeep-Aliveを明示的に設定する例
const https = require('https');
const axios = require('axios');

// エージェント層でコネクションを維持
const agent = new https.Agent({
  keepAlive: true,
  maxSockets: 100, // 同時接続数の上限を制御
  maxFreeSockets: 10,
  timeout: 60000 // 60秒でアイドル接続を切断
});

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  httpsAgent: agent
});

async function fetchData() {
  try {
    const response = await apiClient.get('/v1/data');
    return response.data;
  } catch (error) {
    console.error('通信エラー:', error.message);
  }
}

② インフラ側のスケールアウト対策(AWSの場合)

もしアプリケーション側のコード最適化をやり尽くしてもなおトラフィック量が多い場合は、インフラストラクチャ側のスケールを疑うべきだ。

  • NATゲートウェイの追加とルートテーブルの分割:

AWSのNATゲートウェイは1つあたり最大45Gbpsまでスケールアップするが、「1つのNATゲートウェイあたりの最大ポート数」というボトルネックが存在する。
可用性ゾーン(AZ)ごとにNATゲートウェイを配置し、プライベートサブネットのルートテーブルをAZ単位で分割(ローカルAZルーティング)することで、SNATポートのプールそのものを水平分散させることができる。

—

4. トラブルシューティング:今、何が起きているかを暴くコマンド

最後に、実際に「外向きの通信がおかしいぞ」となったときに、現場のエンジニアがどうやって真実を突き止めるべきか、その手順を共有しよう。

踏み台サーバーや、問題のあるコンテナ内に入って以下のコマンドを叩いてみてほしい。

1. 現在のコネクションとSNATの詰まり具合を確認する (ss または netstat)

# 現在確立されている(またはTIME_WAITの)外向きTCPコネクション数をカウント
ss -s

出力結果の TCP: segs ... や、個別の接続状態を確認する。

# 特定の外部宛先へのコネクションがどの状態にあるかを一覧化
ss -tan '( dst : 203.0.113.50 )'

2. パケットキャプチャで実物を見る (tcpdump)

「本当にNATの手前でパケットが出ているのか」「変なポートを掴んでいないか」を調べるには、やはりtcpdumpが最強の相棒だ。

# プライベートサブネット上のインスタンスから外部APIへ向かうパケットをキャプチャ
sudo tcpdump -nnvvv -i eth0 host 203.0.113.50 and port 443

ここで出力される送信元IP(例: 10.0.1.50:45123)が、NATゲートウェイを通過した後にどう書き換わっているかは、残念ながらAWS側(VPC Flow Logs)を見ないと言えないが、「そもそもインスタンスから意図した頻度でパケットが送出されているか」を切り分けるためには必須のスキルだ。

3. VPC Flow Logsで答え合わせをする

もし「ポート枯渇なのかどうか」を確信したいなら、Amazon CloudWatch LogsやAthenaに流した VPC Flow Logs を確認するのが一番確実だ。
REJECT されたパケットや、srcaddr / dstaddr / srcport / dstport の組み合わせを集計し、特定のNATゲートウェイIPに対するトラフィックが異常値を示していないかクエリを回してみよう。

—

まとめ:パケットの「出口」に愛を注ごう

インフラの設計において、内側(VPC内部やセキュリティグループ)ばかりに気を取られがちだが、外側への「出口」であるNATゲートウェイとSNATの挙動は、大規模システムやマイクロサービスアーキテクチャの成否を握る隠れた要だ。

  • プライベートIPからパブリックIPへの変換は、NATゲートウェイのConntrackとエフェメラルポートの消費の上に成り立っている。
  • ポート枯渇を防ぐ最大の武器は、アプリケーション層での「コネクションプールの維持(Keep-Alive)」である。
  • いざという時は、ss や tcpdump、VPC Flow Logsを駆使して、パケットの足取りを泥臭く追跡する。

この仕組みを頭の片隅に置いておけば、次に「突然API繋がらなくなったんですけど!」と後輩が慌てて駆け込んできても、クールな笑みを浮かべながら「お、じゃあまずコネクションプールとSNATポートの使用率から当たってみようか」と言ってやれるはずだ。

それじゃあ、また現場のトラブルシューティングの海で会おう。健闘を祈る!

コメント

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