【実務・中級編】 AWS NATゲートウェイのアイドルタイムアウト値(350秒)とキープアライブ制御 – クラウド&コンテナネットワーク実践ガイド

こんにちは。SREとして日々数々のクラウドインフラと向き合っていると、時々「なぜか本番環境で特定のバッチ処理やAPI通信が数分おきにしれっと切断される」という、なかなかに香ばしいトラブルに遭遇します。

エラーログを覗いても「Connection reset by peer」や「Timeout」という塩対応なメッセージばかり。アプリケーションコードをいくら睨んでもバグは見つからない。そんなとき、犯人は大抵ネットワークの境界線に潜んでいます。

今回は、AWSのパブリック/プライベートサブネットを繋ぐ縁の下の力持ち、NATゲートウェイの「350秒の呪縛(アイドルタイムアウト)」について、パケットの挙動から具体的なコードでの対策まで、現場の知見を総動員して徹底的に解説します。

—

1. なぜ「350秒」なのか? パケットの裏側で起きていること

まず、AWSのマネージドなNATゲートウェイの仕様からおさらいしましょう。
プライベートサブネットにあるEC2やECSタスクから、NATゲートウェイを介してインターネット上の外部APIへリクエストを飛ばすとき、NATゲートウェイは送信元のプライベートIPアドレスとポートを、自身のパブリックIPアドレスとポートに変換(SNAT)し、ステートフルにコネクションを管理しています。

ここで問題になるのが、RFC 5387などで規定されるネットワーク機器のアイドルタイムアウトです。AWSのNATゲートウェイは、TCPコネクション上で最後にパケットが流れてから「350秒間(約5分50秒)」一切の通信が途絶えると、コネクションのステート情報を問答無用で破棄します。

影の主役:コネクションテーブルの消滅劇

ここで恐ろしいのは、NATゲートウェイが勝手にコネクションを忘れたとしても、通信しているクライアント側(例えばPythonのrequestsやNode.jsのaxios)や、通信相手のサーバー側は「まだコネクションは生きている」と信じ込んでいる点です。

この状態で、350秒以上経過した後にクライアントが突然データを送ろうとパケットを投げたとします。すると、何が起きるでしょうか?

1. クライアントは古いセッション情報のまま、パケットをNATゲートウェイへ送る。
2. NATゲートウェイは「そんなコネクションの記憶はないが?」と、そのパケットをドロップするか、相手にRSTパケットを返す。
3. クライアント側は「Connection reset」や「Timeout」を食らい、阿鼻叫喚の渦に巻き込まれる。

これが、アイドルタイムアウトの正体です。コネクションは常に流し続けていないと、ネットワーク機器のメモリから容赦なく忘却の彼方に追いやられるのです。

—

2. 標準RFCとTCP Keep-Aliveによる防衛策

この理不尽な切断を防ぐ王道のアプローチが、TCP Keep-Aliveの活用です。

TCPの仕様(RFC 793)において、Keep-Aliveは「アイドル状態のコネクションがまだ生きているかを確認するため、定期的に空のパケット(あるいはACKを含むパケット)を送信するメカニズム」として定義されています。

OSレベル、あるいはアプリケーションのHTTPクライアント層でこのKeep-Aliveのインターバルを適切に設定し、「350秒が経過する前に、必ず何かしらのパケットをNATゲートウェイに通過させる(アイドルタイマーをリセットする)」というのが実務における鉄則となります。

安全係数を見込み、実務では 「およそ45秒〜60秒おきにKeep-Alive(またはハートビート)を送る」 という設計にするのがベストプラクティスです。

—

3. 実務で使える!各言語・ツール別の具体的な実装・設定例

ここからは、実際にコードベースや設定ファイルでどのようにこの問題に対処するのか、具体的なサンプルを見ていきましょう。

A. Linux OS全体(kernel)レベルでの設定変更

ECSのEC2起動タイプや、プライベートサブネットに置いた自前で管理するEC2インスタンスの場合、カーネルパラメータ(sysctl)を調整することで、OS全体でTCP Keep-Aliveを前倒しで発動させることができます。

/etc/sysctl.conf に以下の設定を追記します。

# コネクションが無音になってから、最初にKeep-Aliveパケットを送るまでの時間(秒)
# デフォルトは通常7200秒(2時間)なので、350秒未満の「60秒」に変更する
net.ipv4.tcp_keepalive_time = 60

# Keep-Aliveパケットを再送する際の間隔(秒)
net.ipv4.tcp_keepalive_intvl = 10

# 応答がない場合に、切断と見なすまでの再送回数
net.ipv4.tcp_keepalive_probes = 6

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

sudo sysctl -p

—

B. Python (requests / urllib3) での制御

Webスクレイピングや、外部APIへ長時間の接続を維持するPython製バッチアプリケーションの場合、requests.Session と urllib3 のコネクションプール、およびTCPのKeep-Aliveを組み合わせます。

以下は、アダプターを使用してTCP Keep-Aliveを有効にする堅牢なセッションの構築例です。

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

def create_robust_session():
    """
    NATゲートウェイの350秒タイムアウトを回避するため、
    TCP Keep-Aliveと適切なリトライ設定を組み込んだセッションを返します。
    """
    session = requests.Session()

    # リトライ戦略の定義(一時的なネットワークエラーに対応)
    retries = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[500, 502, 503, 504],
        raise_on_status=False
    )

    # HTTPAdapterを用いてプールを設定
    # ※ 標準のrequestsではソケットレベルのTCP Keep-AliveはOS依存ですが、
    #    urllib3の最新バージョンやカスタムアダプターでソケットオプションを渡すことも可能です。
    adapter = HTTPAdapter(
        pool_connections=10,
        pool_maxsize=20,
        max_retries=retries
    )

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

# 使用例
if __name__ == "__main__":
    api_session = create_robust_session()
    
    try:
        # 定期的なAPIリクエストの例
        response = api_session.get("https://api.example.com/v1/resource", timeout=30)
        print(f"ステータスコード: {response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"通信エラーが発生しました: {e}")

—

C. Node.js (axios / http agent) での設定

サーバーレス(AWS Lambda)やECS上のNode.jsアプリケーションから外部APIを叩く場合も注意が必要です。http.Agent または https.Agent を用いて、キープアライブを明示的に有効化します。

const axios = require('axios');
const https = require('https');

// Keep-Aliveを有効にしたHTTPSエージェントの作成
const agent = new https.Agent({
  keepAlive: true,
  // ソケットがアイドル状態のときに、Keep-Aliveパケットを送信するまでの遅延(ミリ秒)
  keepAliveMsecs: 60000, // 60秒
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 60000
});

// axiosインスタンスにエージェントをバインド
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  httpsAgent: agent,
  timeout: 10000
});

async function callExternalApi() {
  try {
    const response = await apiClient.get('/v1/data');
    console.log('データ取得成功:', response.data);
  } catch (error) {
    console.error('API呼び出し失敗:', error.message);
  }
}

callExternalApi();

—

D. 疎通確認用:curlコマンドのオプション

デバッグ時に「自分の環境からNATゲートウェイを挟んだ通信がどう振る舞うか」をサクッと確認したいときは、curl のTCP Keep-Aliveオプション(--tcp-nodelay や詳細なソケット制御)を利用するか、そもそもタイムアウトを避けるための定期実行をテストします。

# TCPのKeep-Aliveを有効にしてリクエストを投げる例
# (curlはデフォルトでOSのTCP Keep-Alive設定に従いますが、明示的にオプションを指定して挙動を確認できます)
curl -v --connect-timeout 10 --max-time 30 https://api.example.com/healthcheck

—

4. 現場のシニアSREが教える、トラブルシューティングの極意

もしあなたが今、「特定の時間帯だけAPIが切れる」「数分放置した後の最初のリクエストだけ必ず失敗する」という怪奇現象に悩まされているなら、以下の手順で即座に原因を切り分けてください。

1. メトリクスの確認: AWS CloudWatchで、該当するNATゲートウェイの ErrorPortAllocation や ConnectionTimedOut メトリクスが跳ね上がっていないか確認する。
2. パケットキャプチャ(tcpdump): アプリケーションサーバー上で tcpdump を仕掛け、FINやRSTパケットがどちらの起点で飛んでいるかを追跡する。

sudo tcpdump -nnvvS host <外部APIのIPアドレス>

3. アプリケーションのライフサイクルを見直す: 「コネクションを張りっぱなしにする設計」になっているか、「毎回コネクションを切断(Short-lived)しているか」を確認する。後者であればそもそもNATのセッション枯渇やタイムアウトは起きにくいですが、コネクションハンドシェイクのオーバーヘッドが増えます。前者の場合は、今回解説したKeep-Aliveが絶対に必須です。

クラウドネットワークの挙動は、時に冷徹で気まぐれに見えますが、すべては物理法則とプロトコル仕様(RFC)の厳密な積み重ねの上で動いています。「350秒」という数字を頭の片隅に置いておくだけで、夜中に突然呼び出されるプレッシャーから解放されるはずです。

さあ、今すぐコードベースのHTTPクライアント設定を見直してみましょう!

コメント

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