クラウドのインフラを設計し、日々の運用でKubernetesやマイクロサービスの海を泳いでいると、避けて通れないのが「ネットワークの機嫌取り」だ。
「あれ、なんでステージング環境から外部の決済APIに繋がらないんだ?」
「ログを見る限り、アプリはリクエストを投げているのに、タイムアウトで沈黙している……」
こんな夜、君ならどこを疑うだろうか? ルートテーブルか? ネットワークACL(NACL)か? それともアプリケーション層のタイムアウト設定か?
だいたいのケースで、真犯人はセキュリティグループ(Security Group: SG)のステートフルな挙動の誤解にある。
今日は、AWSやGCP(GCPの場合はVPCファイアウォールルールだが、概念はほぼ同じステートフルモデルだ)の根幹を支える、ENI(Elastic Network Interface)単位のセキュリティグループについて、パケットの挙動から実務的なトラブルシューティングまで、現場のシニアエンジニアの視点で徹底的に解説しよう。教科書には載っていない「生きた知識」を持ち帰ってほしい。
—
セキュリティグループは「仮想NICの門番」である
まず大前提として、セキュリティグループとは何かを再確認しておこう。
VPCにおけるセキュリティグループは、サブネット単位ではなく、インスタンスの仮想NIC(AWSならENI)単位でアタッチされる仮想ファイアウォールだ。
サブネットレベルでトラフィックを制御するNACLが「マンションのエントランスの自動ドア」だとすれば、セキュリティグループは「各部屋の玄関の鍵」に例えられる。パケットは、ENIに到達する直前(インバウンド)と、ENIから送り出される直前(アウトバウンド)のまさにその瞬間で、この門番による厳格な審査を受ける。
デフォルト拒否の原則(Default Deny)
セキュリティグループの基本思想は極めてシンプルかつ厳格だ。
- インバウンド(Inbound): すべての通信がデフォルトで「拒否(Deny)」されている。明示的に許可ルール(Allow)を追加しない限り、外からパケットは入ってこない。
- アウトバウンド(Outbound): クラウドプロバイダーによってデフォルトが異なるが、AWSのEC2用セキュリティグループのデフォルトは「すべての通信を許可(All Traffic Outbound)」だ。しかし、セキュリティを厳格に担保する環境では、このアウトバウンドもデフォルト拒否に変え、必要な宛先だけを絞るのがプロの作法である。
—
「ステートフル(Stateful)」という魔法の挙動
初心者が最もハマり、同時にベテランがその恩恵に何度も救われるのが、セキュリティグループの「ステートフル動作」だ。
NACLは「ステートレス(Stateless)」である。つまり、インバウンドで通信を許可しても、レスポンス(戻りトラフィック)を通したければ、アウトバウンド側でも明示的に許可ルールを書かなければならない。往復のパケットを両方で管理する必要があるため、NACLのルール管理はすぐにカオス化する。
一方、セキュリティグループはステートフルだ。
どういうことか? インバウンドで許可されて確立されたコネクションの「戻りパケット」は、アウトバウンドのルールに関わらず、自動的に許可されるのだ。逆に、内側から外へ向けて確立したアウトバウンド通信に対する「戻りパケット」も、インバウンドのルールを気にせず自動で通る。
この挙動を、実際のパケットの往来に沿ってシミュレーションしてみよう。
通信フロー(シーケンス)の裏側
Webサーバー(EC2インスタンス)が外部のAPIサーバーとHTTPS通信を行うシーンを想像してほしい。
1. SYNパケットの送信(アウトバウンド)
- アプリケーションが
https://api.example.com(IP:203.0.113.50)に対してリクエストを投げる。 - インスタンスのENIからパケットが出る際、SGのアウトバウンドルールが評価される(デフォルト許可であればそのまま通過)。
2. コネクション追跡(Connection Tracking)の開始
- クラウドのハイパーバイザー層にある仮想ルーター(ステートフルインスペクションエンジン)は、この往信パケットを検知し、「このIPとポートの組み合わせで通信が始まったな」という状態(State)をメモリ上に記憶(コネクション・トラッキング)する。
3. SYN-ACKパケットの受信(インバウンド)
- 外部APIサーバーからの応答パケットがENIに到着する。
- 通常、インバウンドはデフォルト拒否のはずだ。しかし、ハイパーバイザーはコネクション・トラッキングのテーブルを参照し、「あ、これはさっき内側から出て行った通信の正当な返事だな」と即座に判断する。
- インバウンドの許可ルールに一致していなくても、パケットは無条件でスルーされ、インスタンスに到達する。
この仕組みがあるおかげで、私たちは「外から入ってくる通信のポート範囲はこれで、戻りのポートは……」なんて面倒な計算をせずに済んでいるのだ。
—
実務で直面する「落とし穴」とコード例
しかし、このステートフル動作が仇になる瞬間もある。例えば、エフェメラルポート(一時ポート)の枯渇や、厳格なアウトバウンド制限をかけたマイクロサービス環境だ。
ここでは、Python(requestsライブラリ)や curl を使って、外部APIを叩くコンテナやインスタンスを運用している現場を想定しよう。
トラブル事例:アウトバウンドを絞ったらAPI通信が死んだ
セキュリティ要件の監査が入り、「アウトバウンドの通信もすべてホワイトリスト方式にせよ」というお達しが出たとしよう。そこでインフラエンジニアは、次のようなTerraformまたはAWS CLIの設定を投入した。
# 【アンチパターン】アウトバウンドを特定の宛先IP・ポート(443)だけに絞ったつもりの設定
resource "aws_security_group" "strict_egress" {
name name = "strict-app-sg"
description = "Strict egress security group"
# インバウンドは一旦すべて閉じる
ingress = []
# アウトバウンドはHTTPS(443)のみを許可
egress = [
{
description = "Allow HTTPS to outside"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = []
prefix_list_ids = []
security_groups = []
self = false
}
]
}
この設定、一見すると安全で完璧に見える。しかし、このインスタンス上で動くアプリケーションから外部APIへリクエストを投げると、時々、あるいは完全に名前解決や通信エラー(Connection Timeout)が発生する。なぜか?
原因:DNSクエリ(UDP/TCP 53番ポート)の抜け落ち
アプリケーションは https://api.example.com というホスト名で通信しようとする。その際、最初に何をするか? そう、DNSサーバー(社内DNSや 8.8.8.8 など)への名前解決(DNS Query)だ。
DNSクエリは通常、UDP(またはTCP)の53番ポートを使って行われる。
上記のセキュリティグループの egress ルールを見ると、許可されているのは 443 番ポートのみ。53番ポートへのアウトバウンド通信が明示的に拒否されているため、パケットはENIから一歩も外に出られず、アプリはDNSの名前解決すらできずに沈没していたのだ。
正しい設定アプローチ
この問題を解決するには、アウトバウンドルールにDNSの名前解決用トラフィックを明示的に追加するか、あるいはVPC内DNSリゾルバー(AmazonProvidedDNSなど)のIPレンジに絞ったルールを追加する必要がある。
以下は、Pythonスクリプトから外部APIを安全に呼び出す環境を想定した、実用的なセキュリティグループのアウトバウンド設計例だ。
# 例:Python (requests) を使った外部API呼び出し
import requests
from requests.exceptions import RequestException
def call_external_api():
url = "https://api.example.com/v1/data"
try:
# この通信が成功するためには、
# 1. 53番ポート(DNS)のアウトバウンドが許可されていること
# 2. 443番ポート(HTTPS)のアウトバウンドが許可されていること
# の両方が必要不可欠。
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except RequestException as e:
print(f"Network or API Error occurred: {e}")
raise
このコードを実行するインスタンスのSGには、最低限以下のアウトバウンドルールが必要になる。
# AWS CLIでDNSとHTTPSのアウトバウンドを正しく許可する例
# 1. すでに存在するデフォルトの全許可アウトバウンドルールを削除(必要な場合)
# aws ec2 revoke-security-group-egress --group-id sg-xxxxxx --protocol -1 --port -1 --cidr 0.0.0.0/0
# 2. HTTPS (443) のアウトバウンドを許可
aws ec2 authorize-security-group-egress \
--group-id sg-xxxxxx \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# 3. DNS (UDP 53) のアウトバウンドを許可(VPCのローカルDNSサーバーや外部パブリックDNS宛て)
aws ec2 authorize-security-group-egress \
--group-id sg-xxxxxx \
--protocol udp \
--port 53 \
--cidr 10.0.0.0/16 # ※VPC CIDRに合わせて調整、または 0.0.0.0/0
—
ベーシックなようで、実務ではこうした「DNSを忘れてハマる」ようなトラブルが後を絶たない。
現場で役立つ実践的デバッグTips
最後に、インフラの現場で「あ、これセキュリティグループのブロックだな」と瞬時に見抜き、迷わず原因を特定するための実践的なTipsを授けよう。
1. IPレイヤーの疎通確認には nc や telnet ではなく curl -v を使え
ICMP(ping)が通らないからといってネットワークが死んでいるとは限らない。現代のクラウド環境ではセキュリティグループやNACLでICMPが塞がれていることは日常茶飯事だ。
HTTP/HTTPSの通信を確認したいなら、curl -v https://<target-ip> を叩き、TCPのハンドシェイク(Connected to...)まで進むか、それとも Connection timed out で即死するのかを確認せよ。即死する場合は、ほぼ100%セキュリティグループのアウトバウンド、あるいは相手側のインバウンド(または途中のルーティング)でパケットがドロップされている。
2. フローログ(VPC Flow Logs)を有効化し、REJECT パケットを追え
原因不明の通信断に直面したら、迷わずVPC Flow Logs(あるいはCloudWatch Logs Insights)を開き、以下のクエリを投げてみろ。
# CloudWatch Logs Insights用クエリの例
fields @timestamp, srcAddr, dstAddr, dstPort, protocol, action, packets, bytes
| filter action = "REJECT"
| sort @timestamp desc
| limit 20
action = REJECT のレコードを見れば、どの送信元IPから、どの宛先ポート(dstPort)に向かったパケットが、セキュリティグループやNACLによって葬り去られたのかが一目瞭然でわかる。ログは嘘をつかない。
—
まとめ
セキュリティグループは、クラウドネットワークにおける「最後の砦」であり、インスタンスレベルの安全性を担保する最も強力なツールだ。
その「ステートフル動作」のメカニズムと、往復のパケットフロー、そしてDNSなどの暗黙的な依存関係を正しく理解していれば、どんなに複雑なマイクロサービスのネットワーク設計であっても、迷うことはなくなるはずだ。
トラブルシューティングの夜、パケットの気持ちになって「今、こいつはどの門番に止められているんだ?」と想像を巡らせてみてほしい。答えは必ず、そのパケットの足元にある。
コメント