AWS NLBの「クロスゾーン負荷分散」と「クライアントIP保持」の深淵:実務で泣かないためのネットワーク設計指針
現場でインフラを触っていると、「NLB(Network Load Balancer)はL4だから楽勝」と油断した瞬間に、パケットの行方を見失うという経験を一度はするはずです。特にクロスゾーン負荷分散(Cross-Zone Load Balancing)とクライアントIP保存の組み合わせは、Web APIの設計においてパフォーマンスと可用性を左右する、極めて重要な「急所」です。
今回は、教科書には載っていない「パケットの呼吸」を感じながら、NLBの挙動を解剖していきましょう。
—
1. NLBの「クロスゾーン負荷分散」:なぜデフォルトで有効ではないのか?
まず、大前提を整理します。NLBにおいて「クロスゾーン負荷分散」を有効にするか否かは、「トラフィックの局所性(Locality)」と「各AZのターゲット数の不均衡」のトレードオフです。
挙動のリアル
- 無効(デフォルト): クライアントがアクセスしたAZ内のターゲットにのみルーティングされます。パケットはAZを跨がないため、データ転送コスト(AZ間転送料金)が発生しません。しかし、特定のAZにターゲットが集中していると、そのAZがボトルネックになり、負荷の偏りが顕著になります。
- 有効: すべてのターゲットがどのAZに存在していても、NLBは全ターゲットに対して均等に負荷を分散します。AZ間トラフィックが発生するためコストは上がりますが、ターゲットのAZ構成に柔軟性を持たせることができます。
現場の教訓: 大規模なAPI基盤では、ターゲットのAZごとの増減(オートスケーリング)が頻繁に起こるため、基本的には「有効」にしておいた方が、特定のAZへの集中によるパニックを防げます。
—
2. クライアントIPの保存と「Proxy Protocol v2」の深層
NLBはL4ロードバランサーなので、ターゲットとなるサーバーに届くパケットの送信元IPは、通常であれば「NLBのノードIP」になります。これではバックエンドでアクセスログを出力しても、すべてのIPがNLBのものになってしまい、解析不能です。
これを解決するのが「クライアントIPの保持」ですが、実装には2つのパターンがあります。
パターンA:IPターゲットタイプ(推奨)
NLBのターゲットグループを「IP」タイプで作成し、ターゲット(EC2やコンテナ)をVPC内に配置すると、NLBはパケットを改変せず、クライアントのIPを保持したままターゲットまで届けます。これが最もシンプルです。
パターンB:Proxy Protocol v2
ターゲットがNLBの配下にあり、かつNLBのパケットを直接処理できない環境や、L7プロキシを多段に組む場合、Proxy Protocol v2 を有効にします。これは、TCPヘッダーの直前に「クライアントの元IP・ポート情報」をバイナリ形式で挿入する仕様です。
実務での注意点: ターゲットのアプリケーション(NginxやGoの標準ライブラリなど)がこのバイナリヘッダーを解釈できるように設定しなければ、コネクション確立直後にパケットが壊れていると判断され、接続エラー(EOFなど)になります。
—
3. 実践:クライアントIPを正しく取得するための実装例
では、実際にバックエンドのサーバーでクライアントIPを正しく扱うためのコードを確認しましょう。ここではGo言語でTCPサーバーを記述し、Proxy Protocol v2 を読み取る例を示します。
// 現場でよく使う Proxy Protocol v2 のハンドリング例
package main
import (
"fmt"
"net"
"github.com/pires/go-proxyproto" // 標準ライブラリではないため利用
)
func handleConnection(conn net.Conn) {
// 接続時に Proxy Protocol のヘッダーを読み取る
// これを忘れると、最初の数バイトがヘッダーとして解釈されずパケットエラーになる
defer conn.Close()
// 実際のアプリケーション処理
fmt.Printf("接続元IP: %s\n", conn.RemoteAddr().String())
}
func main() {
listener, _ := net.Listen("tcp", ":8080")
// Proxy Protocol 対応リスナーにラップする
proxyListener := &proxyproto.Listener{Listener: listener}
for {
conn, _ := proxyListener.Accept()
go handleConnection(conn)
}
}
—
4. トラブルシューティング:パケットが繋がらない時のチェックリスト
「NLBの設定を変えたら接続が切れた」――そんな時に真っ先に疑うべきポイントを挙げます。
1. セキュリティグループの罠:
NLB自体にセキュリティグループを設定できるようになったのは最近です。クライアントからNLB、そしてNLBからターゲットへのSG設定を確認してください。特に「ターゲット側のSGがNLBからのトラフィックを許可しているか」は必須です。
2. クロスゾーンの非対称性:
クロスゾーンを無効にしている場合、クライアントが接続したAZにターゲットが1台も存在しないと、その通信は即座に Connection Refused となります。curl でデバッグする際は、必ずAZを固定してリクエストを投げ分けてください。
# 特定のAZ(ap-northeast-1a)のサブネットに所属するIPに対して直接curlを打つデバッグ
# これにより、NLBの特定のAZノードが死んでいるのか、ターゲットが悪いのかを切り分ける
curl -I http://<ターゲットのプライベートIP>:8080 -v
3. Proxy Protocolの二重適用:
NLBで Proxy Protocol を有効にし、ターゲット側のNginxでも proxy_protocol on; を設定しているか確認しましょう。もしNLB側で有効なのにターゲット側で未設定だと、HTTPのリクエストとして解釈できず接続が切断されます。
最後に:ネットワークの「透明性」を確保せよ
クラウドにおいて、ネットワークは「魔法」ではありません。NLBの挙動を理解するということは、パケットがどのルートを辿り、どのノードでヘッダーが書き換えられ、最終的にどのAPIコンテナに到達するかを脳内でシミュレーションできるということです。
設定変更を行う際は、必ず tcpdump や VPC Flow Logs で実際のパケットの流れを確認する癖をつけてください。その泥臭い積み重ねこそが、トラブル時に一瞬で原因を特定できる「現場力」に繋がります。
次回の記事では、このNLBの先にある「ターゲットグループのヘルスチェック」に潜む地雷について、深掘りしていこうと思います。それでは、良いエンジニアリングを。
コメント