【実務・中級編】 NLBのクロスゾーン負荷分散の仕様とクライアントIP保存の挙動 – クラウド&コンテナネットワーク実践ガイド

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の先にある「ターゲットグループのヘルスチェック」に潜む地雷について、深掘りしていこうと思います。それでは、良いエンジニアリングを。

コメント

タイトルとURLをコピーしました