こんにちは。SREチームのシニアエンジニアです。
夜中に突然「社内システムのAPIサーバーから外部の決済APIに接続できなくなった!」というアラートが鳴り響き、冷や汗をかいた経験はありませんか? クラウドインフラの現場で幾度となく修羅場をくぐり抜けてきた私ですが、その原因の多くは、セキュリティグループ(SG)の感覚でネットワークACL(NACL)をいじってしまい、パケットの往来を見誤ったことにあります。
特に、パブリックサブネットに配置されたインスタンスからインターネット上の外部APIを叩く際、NACLの「ステートレス」という性質と「エフェメラルポート」の制御を理解していないと、いとも簡単に通信はブラックホールに消えていきます。
今回は、パケットがワイヤー上をどう駆け巡り、NACLのテーブルでどう裁かれているのか。そのリアルな挙動を、実務で即座に使える設定例とともに紐解いていきましょう。
—
1. セキュリティグループとNACLの決定的な違い:「ステートフル」と「ステータスレス」
AWSなどのパブリッククラウドを触り始めたエンジニアが最初にハマる罠が、セキュリティグループ(SG)とNACLのルールの違いです。
- セキュリティグループ(ステートフル):
あなたが「インバウンドでポート443を許可する」ルールを入れると、ルーターやファイアウォール側でコネクションの状態(State)を記憶してくれます。そのため、外部へリクエストを投げた際の戻りの通信(レスポンス)は、アウトバウンドのルールを明示的に書かなくても自動的に許可されます。
- ネットワークACL(ステートレス):
NACLはパケット単位でしか世界を見ません。「往き(インバウンド)」のルールと「帰り(アウトバウンド)」のルールは完全に独立しています。つまり、あなたが外部へリクエストを送り、その返り値を受け取るためには、往きと帰りの両方の方向で、それぞれ適切なポートが許可されていることが絶対条件になります。
この「ステートレス」な特性が、外部API連携などの実務において牙を剥きます。それが「エフェメラルポートの制御」です。
—
2. パケットの往来とエフェメラルポートの現実
では、あなたが管理するパブリックサブネット上のEC2インスタンスから、Pythonやcurlを使って外部のWeb API(例: https://api.example.com/v1/data)を叩いたときのパケットの動きを追ってみましょう。
通信シーケンスのリアル
1. クライアントからのSYN送信(アウトバウンド):
インスタンス(プライベートIP: 10.0.1.100)のアプリケーションが立ち上がり、宛先サーバー(203.0.113.50のポート443)に対してTCP接続を要求します。このとき、OSのネットワークスタックは、送信元ポートとしてOSが動的に割り当てる一時ポート(エフェメラルポート)である 54321 を勝手に選んでSYNパケットを送り出します。
2. サーバーからのSYN-ACK返信(インバウンド):
外部APIサーバーは、10.0.1.100 のポート 54321 に向けてSYN-ACKパケットを返します。
- ここでNACLの判定が入ります! サブネットの境界にあるNACLのインバウンドルールは、外部からのこのパケット(宛先ポート
54321)を「通してよいか」厳しくチェックします。
もし、NACLのインバウンドルールに、高位ポート(エフェメラルポートの範囲)を許可する設定が抜けていると、パケットはここで無慈悲にドロップされます。TCPの3ウェイハンドシェイクは永遠に完了せず、アプリケーションは「Connection timed out」の例外を吐いて沈黙するのです。
—
3. エフェメラルポートの範囲とRFCの変遷
OSが割り当てるエフェメラルポート(動的ポート)の範囲は、OSのカーネルパラメータや仕様によって異なります。
- IANA(Internet Assigned Numbers Authority)の勧告:
49152から65535 - Linux(RHEL / Amazon Linux 2 / Ubuntu 等のデフォルト):
32768から60999(または32768から61000) - 古いLinuxや一部のカスタム設定、あるいはWindowsの古いバージョン:
1024から5000、あるいは1024から65535
クラウドインフラのNACLを設計・運用する現場では、OSごとの微妙な差異や将来的な拡張性を考慮し、安全性を担保するために、原則として 1024 から 65535 の全域 を双方向で許可するのが業界のベストプラクティスとされています。
—
4. 実務で使える!Terraform / AWS CLIによるNACL設定例
それでは、実際にパブリックサブネットで外部APIを安全かつ確実に呼び出せるようにするためのNACL設定を、Terraformコードで見てみましょう。
TerraformによるNACL設定サンプル
以下のコードは、パブリックサブネット用のNACLを作成し、HTTP/HTTPSの送信(アウトバウンド)と、それに伴うエフェメラルポートの戻り通信(インバウンド)を正しく許可する構成です。
resource "aws_network_acl" "public_nacl" {
vpc_id = aws_vpc.main.id
# ----------------------------------------------------
# インバウンドルール (Inbound Rules)
# ----------------------------------------------------
# 1. 外部からのWebアクセス(HTTP)を許可
ingress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# 2. 外部からのWebアクセス(HTTPS)を許可
ingress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# 3. 【最重要】外部API等から返ってくるエフェメラルポートの戻り通信を許可
# ※これがないと、インスタンスから外へ通信した際のレスポンスがすべて捨てられます
ingress {
protocol = "tcp"
rule_no = 120
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# ----------------------------------------------------
# アウトバウンドルール (Outbound Rules)
# ----------------------------------------------------
# 4. 外部へのHTTP通信を許可
egress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# 5. 外部へのHTTPS通信(外部API呼び出し等)を許可
egress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# 6. 【最重要】インスタンスから外部へ向けて発出されるエフェメラルポートの通信を許可
egress {
protocol = "tcp"
rule_no = 120
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
tags = {
Name = "prod-public-nacl"
Environment = "Production"
}
}
—
5. アプリケーション層での検証とトラブルシューティングTips
インフラ側で上記のようにNACLを設定したら、実際にアプリケーションやCLIから疎通確認を行います。現場でよく使われるデバッグ手順をいくつか紹介します。
1. curl による詳細なトレース
外部APIへの疎通がおかしい場合、単に curl を叩くだけでなく、-v(verbose)オプションをつけて名前解決からTCPハンドシェイク、TLSネゴシエーションのどこで止まっているかを把握します。
# 外部APIへのHTTPS接続を詳細ログ付きでテストする
curl -v https://api.example.com/v1/healthcheck
*もし Trying <IP_ADDRESS>... のあと数秒固まって Connection timed out になる場合は、NACLのエフェメラルポート(1024-65535)のインバウンド/アウトバウンドが疑わしいサインです。*
2. Python (Requests) によるAPIコールの実装例
Web API設計やバックエンド実装の現場でよく使われるPythonのコード例です。タイムアウトを適切に設定し、ネットワーク障害時にゾンビプロセス化を防ぐ設計にします。
import requests
from requests.exceptions import Timeout, RequestException
def call_external_api():
api_url = "https://api.example.com/v1/data"
headers = {
"Authorization": "Bearer secret_token_here",
"Content-Type": "application/json"
}
try:
# 接続タイムアウト(3秒)、読み込みタイムアウト(5秒)を設定
response = requests.get(api_url, headers=headers, timeout=(3.0, 5.0))
# ステータスコードが 2xx 以外の場合は例外を発生させる
response.raise_for_status()
return response.json()
except Timeout:
print("エラー: 外部APIへのリクエストがタイムアウトしました。NACLやSGのポート設定、NATゲートウェイのルーティングを確認してください。")
except RequestException as e:
print(f"エラー: 通信中に予期せぬ例外が発生しました: {e}")
if __name__ == "__main__":
result = call_external_api()
print(result)
—
まとめ:シニアからのメッセージ
NACLのステートレス処理とエフェメラルポートの制御は、クラウドネットワークの基礎でありながら、多くのエンジニアが一度はハマる深い落とし穴です。
「セキュリティグループがあるからNACLはデフォルト(全許可)のままでいいや」と放置している現場も多いですが、多層防御(Defense in Depth)の観点や、コンプライアンス要件でサブネット境界のフィルタリングが厳しく求められる現場では、NACLの明示的なチューニングスキルが確実にエンジニアとしての価値を高めてくれます。
パケットの気持ちになり、「今、どのポートを通って、どこへ向かい、どうやって戻ってくるのか」を頭の中でシミュレーションできるようになれば、ネットワークトラブルなど恐るに足らずです。日々のインフラ運用やAPI設計の現場で、ぜひこの知見を役立ててください。
コメント