【実務・中級編】 VPC内のエフェメラルポート範囲(Ephemeral Port Range)とAWSネットワークの仕様 – クラウドインフラと仮想化ネットワーク実践ガイド

AWSネットワークの隠れた罠:エフェメラルポート範囲の仕様と、現場で泣かないためのセキュリティグループ設計

夜中の3時、突然のPagerDutyアラート。
「本番環境のマイクロサービスから外部決済APIへの疎通が一部タイムアウトしています。セーフティネットのオートスケーリングも効いていません」

現場のエンジニアなら、背筋が凍るような瞬間だ。急いでCloudWatchのメトリクスやVPCフローログを確認すると、異常なパケットロスやコネクション枯渇の兆候が見つかる。しかし、アプリケーションコードには問題なく、ルートテーブルやインターネットゲートウェイ(IGW)も正常に稼働している。

犯人は大抵、私たちが日頃その存在を忘れていがちな「エフェメラルポート(一時ポート)」と、それに伴うAWS固有のネットワーク仕様、そしてセキュリティグループ(SG)の設計ミスにある。

今回は、AWSのVPC内におけるエフェメラルポートの挙動を深く紐解き、パケットがどのようにネットワークを駆け巡り、なぜ本番障害を引き起こすのか、そのメカニズムと実践的な対策をシニアSREの視点から徹底解説しよう。

—

1. エフェメラルポートとは何か? パケットの旅路を追う

私たちが普段何気なく使っているHTTPクライアント(curlやPythonのrequests、JavaScriptのfetchなど)が外部のWeb APIにリクエストを投げる時、OSのネットワークスタックは何をしているだろうか?

通信の基本は「送信元IP・送信元ポート」と「宛先IP・宛先ポート」の4つメンバ(4-tuple)による識別だ。宛先側はHTTPSであれば通常 443 ポートで待ち受けているが、クライアント側(AWSのEC2インスタンスなど)は、サーバーからの応答を受け取るための「帰り道」のポートを動的に割り当てる必要がある。これがエフェメラルポート(Ephemeral Port)である。

標準的なLinuxとAWS(Amazon Linux)の仕様差異

Linuxカーネルのデフォルト(RFC 6056準拠)では、エフェメラルポートの範囲は 32768 から 65535 までに設定されていることが多い。しかし、Amazon LinuxのAMIや、ディストリビューション、さらにはOSのバージョン(あるいはWindowsインスタンス)によって、この範囲は微妙に、あるいは劇的に異なる。

  • Amazon Linux 2 / 2023: デフォルトで 32768 から 65535 (約32,768ポート利用可能)
  • 古いLinuxディストリビューションや特定の設定: 1024 から 65535
  • Windows Server: 動的ポート範囲が標準で 49152 から 65535 (またはレジストリで変更可能)

この「利用可能なポート数」が、実は高トラフィックなシステムにおいて致命的なボトルネックになる。

—

2. AWS環境におけるポート枯渇とセキュリティグループの罠

AWSのVPC環境、特にNATゲートウェイやパブリックサブネットのEC2インスタンスから外部へ大量のアウトバウンド通信を行う際、エンジニアが陥りがちな最大の罠が「セキュリティグループ(SG)のアウトバウンドルール設計ミス」だ。

「すべて許可(0.0.0.0/0)」の誘惑とセキュリティ要件の衝突

セキュリティベストプラクティスとして、「アウトバウンドは原則すべて許可(0.0.0.0/0 の ALL)」にすることが多い。しかし、金融や医療系の厳格なコンプライアンス要件を持つシステムでは、「アウトバウンド通信もホワイトリスト方式で特定の宛先IPとポートのみに絞る」ことが求められる。

ここで、次のような設計をしたとしよう。

> 「EC2から外部の決済API(203.0.113.50:443)へHTTPSリクエストを送るため、アウトバウンドルールには 203.0.113.50/32 の 443 ポートのみを許可しよう」

……この設計、実は通信が失敗する、あるいは予期せぬパケットロスを引き起こす可能性が高い。 なぜなら、OSが割り当てるエフェメラルポートの存在を忘れているからだ。

パケットキャプチャの現場から:SYNパケットの行方

クライアント(EC2)が外部APIへ接続する際、通信の起点となるパケットの宛先ポートは 443 である。しかし、送信元ポートはOSによってランダムに選ばれたエフェメラルポート(例: 54321)になる。

もしセキュリティグループのアウトバウンドルールで「宛先ポート 443 のみ」を許可している場合、OSがエフェメラルポートを使って送信しようとしたTCPの SYN パケットは、AWSのハイパーバイザー層のセキュリティグループ評価でドロップされる可能性がある。なぜなら、パケットの「宛先ポート」は 443 だが、インスタンスから外に向かう通信において、SGのアウトバウンドは「トラフィックが出ていく側のポート(=送信元ポート)」を厳密に制限する機能ではないが、ステートフルな戻り通信の評価や、宛先ポートベースの制限と混同して誤った設定をしがちだからだ。

正確には、セキュリティグループのアウトバウンドルールにおけるポート指定は、「通信が向かう先の宛先ポート(Destination Port)」を指す。したがって、203.0.113.50 の 443 への通信であれば、アウトバウンドで 443 を許可していれば、OSがどのエフェメラルポート(送信元)を使おうとも、アウトバウンド自体は通過する。

真の罠は「インバウンドルール(戻り通信)」と「NATゲートウェイ等のSNAT」にある。

—

3. コードと検証:エフェメラルポートを意識した実装例

実際にアプリケーションから外部APIを叩く際、コネクションの張りっぱなし(Keep-Alive)を意識しないと、瞬く間にエフェメラルポートが枯渇する。

以下のPython(requestsライブラリ)のコード例を見てほしい。悪い例と良い例を比較する。

【アンチパターン】コネクションを再利用しない実装

毎回新しいセッションを作るコードは、短時間に大量のリクエストを投げると、TIME_WAIT状態のソケットがポートを占有し、ポート枯渇を引き起こす。

import requests

# 毎回requests.getを使うと、内部で新しいセッション(TCPコネクション)が張られ、
# 終了後にTIME_WAIT(通常60秒)としてエフェメラルポートがロックされる。
def call_external_api_bad(url):
    for i in range(1000):
        # 毎回のリクエストで新しいエフェメラルポートが消費される
        response = requests.get(f"{url}/item/{i}", timeout=5)
        print(response.status_code)

【ベストプラクティス】Sessionオブジェクトによるコネクションプーリング

requests.Session() を使い、HTTP Keep-Aliveを有効にすることで、既存のTCPコネクションを再利用し、エフェメラルポートの消費を最小限に抑える。

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

def call_external_api_good(url):
    # セッションを作成し、コネクションプールとリトライ戦略を定義する
    session = requests.Session()
    
    # リトライ設定(ネットワークの瞬断対策)
    retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
    adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=retries)
    
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    try:
        for i in range(1000):
            # 同じTCPコネクション(同一エフェメラルポート)が再利用される
            response = session.get(f"{url}/item/{i}", timeout=5)
            print(response.status_code)
    finally:
        session.close()

Node.js(Fetch API / Axios)やPHP(cURL)を使う場合も同様に、Keep-Alive(Agentの適切な設定)が有効になっているかを確認することが、SREとしての第一歩だ。

—

4. Linuxカーネルパラメータ(sysctl)のチューニング手法

もし、どうしても高トラフィックでエフェメラルポートが足りなくなる場合や、負荷テスト時に Address already in use や Cannot assign requested address というエラーに直面した場合は、Linuxのカーネルパラメータをチューニングする必要がある。

実務で即座に使える設定ファイルを以下に示す。/etc/sysctl.d/99-ephemeral-ports.conf などのファイル名で配置し、適用するのがモダンな作法だ。

# /etc/sysctl.d/99-ephemeral-ports.conf
# エフェメラルポートの範囲を拡張する(デフォルトの32768-65535から、利用可能な領域を広げる)
# 注意: 1024未満は特権ポートのため、通常は1024からにするが、他のミドルウェアが使用していないか確認すること
net.ipv4.ip_local_port_range = 1024 65535

# TIME_WAIT状態のソケットを迅速に再利用することを許可する(セキュリティ上のリスクが極めて低い環境で有効)
net.ipv4.tcp_tw_reuse = 1

# FIN-WAIT-2状態のタイムアウト時間を短縮し、リソースの解放を早める
net.ipv4.tcp_fin_timeout = 15

設定を反映するには、以下のコマンドを実行する。

# 設定を即時反映させる
sudo sysctl --system

ただし、net.ipv4.ip_local_port_range を広げる場合、内部で稼働しているカスタムアプリケーションやデータベース(MySQLやPostgreSQLなど)がバインドしようとする固定ポートとバッティングしないか、事前のポートスキャンや設計確認が不可欠である。

—

5. 現場のトラブルシューティングTips

最後に、本番障害でエフェメラルポート枯渇やネットワークの異常を疑った際、現場のSREがどのような手順で調査すべきか、その実用的なコマンド集を授けよう。

1. 現在のソケットの状態とポート消費量を瞬時に確認する

EC2インスタンスにSSM Session Manager等でログインし、どの状態のコネクションが何個あるのかを ss コマンド(旧 netstat)で集計する。

# 状態ごとのソケット数を集計して降順で表示する
ss -s

あるいは、特定の外部IPへの接続状況を詳細に見るには:

# 外部APIのIP(例: 203.0.113.50)に対するコネクション一覧とエフェメラルポートの使用状況
ss -tan '( sport = :http or sport = :https )' | grep 203.0.113.50

2. NATゲートウェイのバースト制限に注意する

AWSのNATゲートウェイは、1つのIPアドレスあたり最大55,000同時コネクション(ターゲットへの接続)をサポートしている。もし単一のNATゲートウェイを経由して数千台のEC2から外部APIへ大量リクエストを送る場合、エフェメラルポートの枯渇はインスタンス側だけでなく、NATゲートウェイのSNATポートの枯渇として現れる。
CloudWatchメトリクスで ErrorPortAllocation がスパイクしていないか必ず確認しよう。もし枯渇している場合は、NATゲートウェイの数を増やす、あるいは複数のパブリックIPを割り当てる設計変更が必要になる。

—

まとめ

エフェメラルポートは、普段は黒衣として静かに通信を支えている存在だが、システムがスケールした途端に牙をむく。
「なぜかランダムにタイムアウトする」「高負荷時に突然エラーが増える」という現象に直面したときは、コードのバグを疑う前に、OSのエフェメラルポート範囲、Keep-Aliveの設定、そしてAWSのネットワーク(NAT GatewayやSecurity Group)の仕様を思い出してほしい。

インフラとアプリケーションの境界線を正しく理解し、パケットの往来を頭の中でイメージできるようになれば、どんな難解なネットワーク障害も怖くはないはずだ。さあ、今夜は安心して眠りにつこう。

コメント

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