NLBで「IPアドレスが消える」現象に終止符を。透過的フォワーディングの深淵と実務的Tips
こんにちは。クラウドインフラの深淵を覗き続けて10年、今日もどこかでパケットの海を泳いでいるSREです。
AWSのロードバランサー選定において、ALB(Application Load Balancer)は「L7の賢い奴」、NLB(Network Load Balancer)は「L4の速い奴」という認識で止まっていませんか?しかし、現場で「クライアントのIPアドレスをログに取りたい」「SSHを特定IPに制限したい」という要望が出た瞬間、NLBの挙動の深さに頭を抱えるエンジニアを何人も見てきました。
今回は、NLBがなぜ「魔法のように」クライアントIPを透過させ、バックエンドに届けるのか。その内部構造と、現場で必ず直面する落とし穴について解説します。
—
1. なぜ「透過的」なのか? ALBとNLBの決定的な違い
ALBは「リバースプロキシ」です。クライアントから受け取ったパケットはALBで一度終端され、ALBがバックエンドに対して「新しいパケット」を作成して投げます。そのため、バックエンドから見える送信元IPは常にALBのIPになり、クライアントの真のIPを知るには X-Forwarded-For ヘッダーを解析するしかありません。
対してNLBは、L4ロードバランサーとして「フローの維持」を最優先します。TCPリスナーの場合、NLBはパケットを終端させず、IPヘッダーやTCPヘッダーを書き換える(NAT / DNAT)ことでバックエンドへ転送します。
透過的フォワーディングのフロー
1. Client -> NLB: 送信元 1.2.3.4 (Client) -> 送信先 5.6.7.8 (NLB)
2. NLB -> Target: 送信元 1.2.3.4 (Client) -> 送信先 10.0.1.5 (Target)
このとき、NLBはIPアドレスを保持したまま(透過的に)パケットを届けるため、バックエンドのアプリケーションは、まるでクライアントから直接アクセスが来たかのように処理できるのです。これが「透過的フォワーディング」の正体です。
—
2. 実務で遭遇する「落とし穴」と設定の極意
この透過的な挙動は強力ですが、一方で「パケットが戻ってこない」という泥沼のトラブルを引き起こす原因にもなります。
ターゲットタイプによる挙動の差
NLBのターゲットグループ設定において、Instance タイプと IP タイプで挙動が異なることはご存知でしょうか?
- Instanceタイプ: クライアントIPを保持。
- IPタイプ: クライアントIPを保持。ただし、バックエンドがVPC内か、あるいはAWS PrivateLink経由かでルーティングが複雑になる。
もっとも注意すべきは、「NLBと同じVPC内にあるバックエンドにパケットが戻る際、NLBを通らずに直接クライアントに返そうとして捨てられる」ケースです。これを防ぐには、ターゲット側でNLBがパケットを転送したことを意識したルーティングが必要です。
実際に curl で確認する
NLB経由のアクセスが本当にクライアントIPを保持しているか、バックエンドに簡単なPythonサーバーを立てて確認してみましょう。
# simple_server.py
# サーバー側で受け取った送信元IPを表示するスクリプト
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
# 接続元のIPアドレスを抽出
client_ip = self.client_address[0]
print(f"Connection from: {client_ip}")
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello from Backend!")
server = HTTPServer(('0.0.0.0', 80), RequestHandler)
server.serve_forever()
これをEC2で実行し、NLB経由でアクセスすると、ログにはNLBのIPではなく、あなたのローカルPCのグローバルIPが刻まれます。
—
3. セキュリティグループと Preserve Client IP
「SSHをNLB経由で通したい(ポート22)」という相談もよく受けます。ここで重要になるのが、ターゲットグループの属性にある preserve_client_ip.enabled です。
Terraformでの設定例
resource "aws_lb_target_group" "ssh_tg" {
name = "ssh-target-group"
port = 22
protocol = "TCP"
vpc_id = var.vpc_id
target_type = "instance"
# クライアントIPを保持する設定(デフォルトで有効)
target_group_attributes {
key = "preserve_client_ip.enabled"
value = "true"
}
}
注意: preserve_client_ip を有効にすると、バックエンドのインスタンス側では、そのクライアントIPからの通信を許可するセキュリティグループ設定が必要です。NLBのIPを許可していても、バックエンドは「見知らぬIP(クライアント)」からの通信として遮断してしまうからです。
—
4. SREとしてのデバッグ手順
もし疎通がうまくいかない場合、以下の順序で切り分けを行ってください。
1. NLBのメトリクスを確認: TCP_Client_Reset_Count が急増していないか?
- これが上がっている場合、バックエンド側が接続を拒否している(セキュリティグループの問題)か、戻りパケットのルーティングが失敗しています。
2. tcpdumpでバックエンドのインターフェースを監視:
# バックエンドインスタンスで実行
sudo tcpdump -ni eth0 port 80 or port 443 or port 22
- パケットがバックエンドに届いているのに応答がない場合、アプリケーション層のログを確認しましょう。
3. セキュリティグループの「自分自身」参照:
- ターゲットが複数ある場合、NLB経由の通信を許可するために、セキュリティグループのインバウンドルールに「クライアントのIP範囲」を許可する必要があります。
—
最後に:ネットワークを「信じすぎない」こと
NLBの透過的フォワーディングは非常に強力ですが、インフラ設計を複雑にする諸刃の剣でもあります。特にコンテナ環境(EKS等)でNLBを使う場合、externalTrafficPolicy: Local との組み合わせで挙動が変わるなど、さらに沼が深まります。
「なぜパケットが届かないのか?」という問いに対して、パケットの送信元・宛先・TCPフラグを一つずつ追う泥臭い作業こそが、最も確実な近道です。この記事が、あなたのネットワークトラブルシューティングの一助となれば幸いです。
それでは、また次回の深掘りでお会いしましょう。パケットの旅路に幸あれ!
コメント