クラウドインフラの現場で、夜中に突然 pagerduty が鳴り響く。
「Web APIのレイテンシが跳ね上がっている」「一部のクライアントからのリクエストが Connection Timed Out で死んでいる」。
こういう修羅場をくぐり抜けてきたSREなら、誰もが一度はこう叫びたくなったことがあるはずだ。「おい、セキュリティグループ(以下、SG)のインバウンドは空けたのに、なんでアウトバウンドでパケットがドロップしてんだよ!」と。
大抵の場合、犯人は「SGはステートフルだから、入ってきたものは自動で出ていけるはず」という、表面的な理解のまま放置されたアウトバウンドルールの設計ミス、あるいはLinuxカーネルのコネクション追跡機構のキャパシティオーバーだ。
今日は、AWSのVCPやGCPのVPCファイアウォール(※GCPはデフォルトでステートフルだが、ステートレスなファイアウォールルールも混在できる)の根幹をなす「ステートフル処理とコネクション追跡(Connection Tracking)」のメカニズムについて、パケットの挙動を脳内再生しながら徹底的に紐解いていこう。
—
1. なぜ「ステートフル」なのか?(RFCとパケットの旅路)
インターネットの基本プロトコルであるIP(Internet Protocol)は、そもそも「ステートレス」だ。ルーターやパケットフィルターは、目の前を通り過ぎる1つひとつのパケットが「新しい通信の始まり」なのか「さっきの通信の返事」なのかを、基本的には知らない。
もしAWSのSGが完全なステートレスだったらどうなるか?
Web APIサーバー(例えばポート 443)に対して外部からリクエストを受け入れるために、インバウンドルールで TCP 443 を開けるのはいいとして、その応答パケット(サーバーからクライアントへの返信)を返すために、アウトバウンドルールでも「すべてのエフェメラルポート(一時ポート: 1024-65535)への送信」を明示的に許可しなければならなくなる。
想像してほしい。マイクロサービスが乱立する現代のクラウド環境で、すべてのインスタンスのアウトバウンドをフルオープンにするか、あるいは通信相手ごとに細かく一時ポートのルールメンテをする地獄を……。
ここで登場するのが ステートフル処理(Stateful Inspection) だ。
コネクション追跡(Conntrack)の魔術
SGは、ハイパーバイザーの仮想スイッチ層(AWSであればNitroチップやホストOS上の仮想ネットワークレイヤー)で、Linuxカーネルの netfilter(Conntrack機構)と酷似したステート管理テーブルを持っている。
1. NEW(新規): クライアントからSYNパケットが飛んできた。インバウンドルールに合致するか? OKなら、この通信の「状態(State)」をメモリ上のトラッキングテーブルに記録する。
2. ESTABLISHED(確立済み): 一度 NEW が許可されると、サーバー側が返す応答パケット(アウトバウンド)は、トラッキングテーブルに「すでに許可されたセッションの戻りパケットだ」と即座に認識される。アウトバウンド側のルールがどう設定されていこうとも、このパケットは自動的に通過を許可される。
これが、私たちがインバウンドを設定するだけで、意識せずにスムーズな双方向通信を行えている理由だ。
—
2. 実務でハマる!ステートフル処理の罠とアンチパターン
しかし、この「自動で許可される」という便利さが、時に私たちを罠に嵌める。現場でよくある失敗パターンをいくつか共有しよう。
トラップ1: アウトバウンドを「全拒否(Explicit Deny)」にしている勘違い
「セキュリティをガチガチにするために、アウトバウンドは全部塞いで、必要なドメインだけに絞ろう」というアプローチをとるチームがある。
SGは原則としてホワイトリスト方式(デフォルト拒否)だが、デフォルトのSGアウトバウンドルールは 0.0.0.0/0 のすべてのトラフィックを許可している。これを削除し、カスタムルールを入れる際にミスを犯す。
例えば、内部のAPIサーバーから外部の決済APIを叩くシナリオを考えてみよう。
- インバウンド: 誰もこのサーバーに外部から直接アクセスしてこないので、インバウンドルールは実質不要(または内部からのアクセスのみ)。
- アウトバウンド: 決済APIのドメインへ
443を許可する。
ここで「サーバーから外部へのリクエストなんだからアウトバウンドだけ設定すればいいや」と思うかもしれないが、もしこのサーバーが「外部からのWebhookを受け取るサーバー」としての役割も兼ねていたらどうなるか?
外部からインバウンドで飛んできたリクエストに対する応答(レスポンス)はステートフルに処理されるため問題ない。しかし、サーバー自身が自発的に外へコネクションを張りに行く場合、その「往き(SYN)」はアウトバウンドルールで評価される。
トラップ2: エフェメラルポートの枯渇とコネクション追跡の限界
高負荷なWeb APIサーバーや、大量のHTTP/HTTPSリクエストを外部へ非同期で投げまくるワーカーノード(Pythonの requests や Node.js の fetch を大量に叩く構成)では、これがボトルネックになる。
TCPコネクションが TIME_WAIT 状態になり、短時間で数万のエフェメラルポートが消費されると、ホスト側のConntrackテーブルが溢れ返る。
テーブルが上限(nf_conntrack_max など)に達すると、新しいパケットは NEW 状態として記録できなくなり、容赦なくドロップ(パケットロス)される。アプリケーション側からは「突然APIがタイムアウトする謎の現象」として観測されることになる。
—
3. コードと設定で見るコネクションのライフサイクル
実際に、Python(requests ライブラリ)と、AWS CLI / Terraform を用いた実務的な設定例を見ていこう。
A. Pythonでのリクエストと持続的接続(Keep-Alive)の重要性
高負荷なAPI設計において、毎回TCPの3ウェイハンドシェイク(SYN -> SYN-ACK -> ACK)とコネクション追跡のエントリ作成・削除を繰り返すのは、ネットワークにとってもCPUにとっても重労働だ。
HTTP Keep-Alive(コネクションプーリング)を使い、既存のコネクション(ESTABLISHED)を再利用することが、Conntrackテーブルを保護する最大の防御策となる。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_robust_api_client():
"""
コネクションプーリングとリトライロジックを実装したセッションを生成する。
Keep-Aliveを利用することで、Conntrackテーブルへの負荷を劇的に軽減する。
"""
session = requests.Session()
# リトライ戦略の設定
retries = Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504],
raise_on_status=False
)
# アダプターを設定(プールサイズを多めに確保)
adapter = HTTPAdapter(
pool_connections=50,
pool_maxsize=100,
max_retries=retries
)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# クライアントの初期化
api_client = create_robust_api_client()
try:
# 外部APIへのリクエスト(すでに確立されたコネクションがあれば使い回される)
response = api_client.get("https://api.example.com/v1/data", timeout=5.0)
if response.status_code == 200:
print("API呼び出し成功:", response.json())
else:
print(f"APIエラー: Status Code {response.status_code}")
except requests.exceptions.RequestException as e:
# タイムアウトやネットワーク層のエラーハンドリング
print(f"ネットワークエラーが発生しました: {e}")
B. Terraformによる安全なSG(セキュリティグループ)の定義
実務でのインフラコード(IaC)では、アウトバウンドを厳格に絞りつつ、ステートフルの恩恵を正しく受ける記述が求められる。以下の例は、踏み台サーバーや外部APIクライアントのための堅牢なSG定義だ。
# セキュリティグループの定義
resource "aws_security_group" "api_client_sg" {
name - "api-client-strict-sg"
description - "Outbound controlled Security Group for API Worker"
vpc_id - var.vpc_id
# インバウンドルール:このインスタンスは外部からの直接アクセスを一切許可しない
# (ステートフルなので、ここを空にしていても、自身が発信した通信の応答は必ず返ってくる)
ingress = []
# アウトバウンドルール:信頼できる外部APIのエンドポイント(例: 443ポート)のみを許可
egress = [
{
description - "Allow HTTPS outbound to trusted payment API"
from_port - 443
to_port - 443
protocol - "tcp"
cidr_blocks - ["203.0.113.50/32"] # 宛先IPを厳格に限定
ipv6_cidr_blocks - []
prefix_list_ids - []
security_groups - []
self - false
},
{
description - "Allow DNS resolution (TCP/UDP 53) to VPC Resolver"
from_port - 53
to_port - 53
protocol - "udp"
cidr_blocks - [var.vpc_dns_resolver_ip] # VPC内部のDNSサーバー等
ipv6_cidr_blocks - []
prefix_list_ids - []
security_groups - []
self - false
}
]
tags = {
Name - "api-client-strict-sg"
Environment - "production"
}
}
—
4. 現場のSREが教える!トラブルシューティングの奥義
もし、本番環境で「パケットがどこでドロップしているか分からない」という事態に陥ったら、以下のステップでデバッグを進めてほしい。
1. VPCフローログ(VPC Flow Logs)を確認する
REJECTログが出ているか確認する。- ステータスが
OKであればSGは通っている。もし捨てられているなら、dstaddrやdstportが想定通りのルールにヒットしているか、srcport(エフェメラルポート)が意図せずブロックされていないか(アウトバウンドルールの抜け漏れ)をチェックする。
2. OS内部のConntrack溢れを疑う(Linuxホストの場合)
- EC2やGCEのインスタンスにSSH/SSMでログインし、以下のコマンドでカーネルの追跡状況を覗いてみる。
# 現在のコネクション追跡のエントリ数と上限を確認
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
- もし
countがmaxに張り付いているなら、アプリケーション側でのコネクションプーリングの見直し、あるいはカーネルパラメータ(net.netfilter.nf_conntrack_max)のチューニング、さらには不要なコネクションの早期切断(TCP Keepaliveの適切な設定)が必要だ。
3. 非対称ルーティング(Asymmetric Routing)の罠に気づく
- 複数のNIC(ENI)を持つインスタンスや、複雑なルートテーブル(VPN/Direct Connect経由など)を組んでいる環境では、「往き」と「戻り」で異なるパスを通ることがある。
- ステートフルなファイアウォールは、往きと戻りが同じ物理/仮想パスを通り、同じステートテーブルを参照することを前提としているため、非対称ルーティングが発生すると、片側のパスで「返ってきたパケットが
ESTABLISHEDとして認識されず、UNKNOWNとしてドロップされる」という悪夢のような現象が起きる。これを見つけたら、ルートテーブルやポリシーベースルーティング(PBR)の設計を見直そう。
—
まとめ
セキュリティグループのステートフル処理は、私たちのクラウドネットワーク設計を劇的にシンプルにしてくれる強力な機能だ。しかし、「裏側で何が起きているか(Conntrackテーブルの存在やパケットのステート遷移)」を理解していないと、スケールした瞬間に予期せぬ障害を引き起こす諸刃の剣でもある。
「インバウンドを開ければアウトバウンドは自動で通る」という呪文の裏側にあるパケットのドラマを想像できるようになれば、あなたも立派なクラウドネットワーク・マスターだ。次のアーキテクチャ設計や障害対応の際には、ぜひこのステートのライフサイクルを思い出してほしい。
コメント