【テクニカル・上級編】 マルチAZ構成におけるNATゲートウェイの冗長化とゾーン障害時のフェイルオーバー – クラウド&コンテナネットワーク実践ガイド

マルチAZ時代のNATゲートウェイ設計:パケットが語る冗長化の真実とゾーン障害時の生存戦略

クラウドネイティブなインフラストラクチャにおいて、「可用性」という言葉はもはや単なるマーケティング用語ではない。それは、夜中に pager が鳴り響くのを防ぐための防壁であり、ミリ秒単位のレイテンシー最適化と表裏一体をなすエンジニアリングの極みだ。

特に、インターネットへの外向き通信(Egress)を担うNATゲートウェイの設計は、システム全体のスループットと耐障害性を左右するクリティカルな要素である。今回は、AWSやGCPなどのメガクラウド環境におけるマルチAZ構成のNATゲートウェイ配置と、ゾーン障害時におけるパケットレベルの挙動、そして極限のパフォーマンスを引き出すためのカーネルおよびTCPチューニングの深淵へと踏り込む。

—

1. なぜ「1つでは足りないのか」:マルチAZ NATゲートウェイの必然性

コスト削減の悪魔の囁きに負け、「プライベートサブネットからの外向き通信は、単一のAZに配置した1つのNATゲートウェイに集約すればいいや」と設計したことはないだろうか。そのアーキテクチャは、特定のAZが物理的な障害(電源喪失やバックボーンの断絶)に見舞われた瞬間、システム全体を沈没させる単一障害点(SPOF)へと変貌する。

さらに、単一NATゲートウェイ構成には、可用性の問題だけでなく「AZ間のデータ転送コスト(Cross-AZ Data Transfer Cost)」という致命的な代償が伴う。異なるAZに存在するEC2インスタンスから単一AZのNATゲートウェイへトラフィックを流し込むと、クラウドプロバイダーは容赦なくAZ間転送量を課金してくる。大規模なマイクロサービス群において、これはじわじわとインフラ予算を蝕む隠れた癌となる。

真にレジリエントな設計とは、すべての可用性ゾーン(AZ)に独立したNATゲートウェイをデプロイし、トラフィックを完全に局所化(Zone-Local Routing)させることである。

[ AZ-a ]                                    [ AZ-b ]
  EC2 (Private) --(同一AZ内ルーティング)--> NAT Gateway-a --> Internet
       | (ゾーン障害時)
       v (動的ルーティング切替)
  EC2 (Private) --(クロスAZ)-------------> NAT Gateway-b --> Internet

—

2. ゾーン障害発生時、ルートテーブルとパケットはどう動くのか

ある日突然、AZ-a がブラックアウトしたとする。この時、パケットの運命はどうなるのか。その裏側では、クラウドのコントロールプレーンとLinuxカーネルのネットワークスタックが緻密な連携を繰り広げている。

コントロールプレーンの挙動

パブリッククラウドのマネージドNATゲートウェイは、背後で冗長化されたElastic IP(EIP)や外部IPを保持している。特定のAZが物理的に隔離された場合、クラウドの自動修復機能(あるいはBGPベースのルーティング制御)が働き、影響を受けたAZのルートテーブルやエンドポイントのステータスが更新される。

しかし、完全な自動フェイルオーバーを待つだけでは、RTO(目標復旧時間)数秒〜数分の間にパケットがブラックホールに吸い込まれてしまう。ここで重要になるのが、インフラストラクチャ・アイズ・コード(IaC)やカスタムヘルスチェックによる、動的なルートテーブルの書き換えメカニズムだ。

データプレーン(パケットレベル)の挙動

AZ-a にあるプライベートサブネットのルートテーブルが、障害検知トリガーによって以下のように書き換わると仮定しよう。

# 障害発生前のルートテーブル (AZ-a用)
- Destination: 0.0.0.0/0
  Target: nat-gateway-az-a (DEAD)

# 障害発生後の動的ルーティング切替後
- Destination: 0.0.0.0/0
  Target: nat-gateway-az-b (ALIVE)

この瞬間、Linuxカーネルのルーティングキャッシュおよびコンラート(Conntrack)テーブルはどうなるか。TCPコネクションの確立中にNATゲートウェイが切り替わると、既存のTCPセッションはソースIP/ポートの維持(あるいはステートフルファイアウォールのセッション断)により、基本的にはリセット(RST)されるか、タイムアウトに向かう。

ここで重要なのは、アプリケーション層での再送メカニズム(Exponential Backoff)と、TLSセッションのレジューム(Session Resumption)の設計である。フェイルオーバー発生時、クライアント側のTCPスタックは即座に新しいルートに向けてSYNパケットを再送し、NATゲートウェイのSNAT(Source Network Address Translation)エンジンが新しいエフェメラルポートを割り当てる。

—

3. 極限のパフォーマンスチューニング:RTT削減とTCPバッファ

マルチAZ構成を採用し、クロスAZのオーバーヘッドを排除したとしても、NATゲートウェイ自体のスケーリング限界やLinuxカーネルのデフォルトパラメータに足元をすくわれることがある。数万コネクションを捌く高負荷なEgress環境では、以下のカーネルパラメータとネットワーク設定が命運を分ける。

Linuxカーネルパラメータ(sysctl)の最適化

NATゲートウェイを自前で構築する場合(あるいは高度なコンテナネットワーク制御を行う場合)、/etc/sysctl.conf において以下のチューニングが不可欠となる。

# エフェメラルポートの範囲を極限まで拡大し、ポート枯渇(Port Exhaustion)を防ぐ
net.ipv4.ip_local_port_range = 1024 65535

# TIME_WAIT状態のソケットを迅速に再利用し、高頻度なアウトバウンド通信に対応
net.ipv4.tcp_tw_reuse = 1

# TCPパケットのウィンドウサイズを動的にスケーリングし、大規模なBDP(Bandwidth-Delay Product)に対応
net.ipv4.tcp_window_scaling = 1

# コネクション追跡(Conntrack)テーブルの最大サイズを引き上げ、高トラフィック時のパケットドロップを回避
net.netfilter.nf_conntrack_max = 2097152
net.netfilter.nf_conntrack_buckets = 524288

ポート枯渇の悪夢とSNATの数理

1つのパブリックIPアドレス(NATゲートウェイ)が同時に確立できるTCPコネクション数は、理論上 (65535 - 1024) = 64,511 が上限となる(実際には宛先IP/ポートの組み合わせである4タプル (Source IP, Source Port, Dest IP, Dest Port) で一意に決まるため、同一宛先でなければこの制限を超えることは可能だが、単一宛先への大量リクエストでは容易に枯渇する)。

これを回避するため、マルチAZ構成の各NATゲートウェイに対して複数個のEIPを割り当て、パブリックIPのプールを拡張するアプローチが極めて有効である。AWSのNATゲートウェイはデフォルトでプレフィックス単位の拡張や複数IPの紐付けをサポートしていないケースもあるが、KubernetesのEgress GatewayやEnvoyを用いたカスタムNATレイヤーでは必須の知見となる。

—

4. セキュリティとトランスポート層の最適化

NATゲートウェイを通過するトラフィックの多くは、HTTPS(TLS 1.3)で暗号化されている。ここでインフラエンジニアが意識すべきは、「暗号化そのものがネットワークスタックに与える影響」と「セキュリティ境界の維持」だ。

TLS 1.3とハンドシェイクのレイテンシー

TLS 1.3では、従来のTLS 1.2と比較してハンドシェイクのラウンドトリップ(RTT)が 2-RTT から 1-RTT(さらに0-RTT Resumption)へと短縮された。しかし、NATゲートウェイでパケットの断片化(Fragmentation)やMTUのミスマッチが発生すると、TCPの初期輻輳ウィンドウ(Initial Congestion Window: initcwnd)のサイズを超えるパケットがドロップし、ハンドシェイクが再送のループに陥る。

特に、クラウド環境のMTU(標準的な 1500 バ이트、あるいはJumbo Frameの 9001 バイト)と、インターネット側のMTUの乖離によって発生する「Path MTU Discovery (PMTUD) のブラックホール問題」は、NATゲートウェイのICMPドロップ設定を誤るとデバッグが極めて困難になる。

以下のiptables(またはnftables)ルールを確認し、ICMP Destination Unreachable( Fragmentation Needed)が適切にフォワードされているか確認してほしい。

# PMTUDを維持するため、ICMP Fragmentation Neededパケットのブラックホール化を防ぐ
# (通常、クラウドのマネージドNATでは自動処理されるが、独自構築のルーターやFirewallでは必須)
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

この設定により、TCPのMSS(Maximum Segment Size)がパスの最小MTUに合わせて自動的にクランプされ、パケットの断片化によるドロップを防ぐことができる。

—

5. 実践:TerraformによるマルチAZ対応NATゲートウェイの模範実装

机上の空論で終わらせないために、インフラストラクチャをコードとして定義する際の実装パターンを示そう。以下のTerraformコードは、可用性とゾーン局所性を担保したマルチAZ NATゲートウェイの設計パターンである。

# 可用性ゾーンのリストを取得
data "aws_availability_zones" "available" {
  state = "available"
}

# 各AZごとに独立したEIPとNATゲートウェイをプロビジョニング
resource "aws_eip" "nat" {
  count  = length(data.aws_availability_zones.available.names)
  domain = "vpc"

  tags = {
    Name = "nat-eip-${data.aws_availability_zones.available.names[count.index]}"
  }
}

resource "aws_nat_gateway" "main" {
  count         = length(data.aws_availability_zones.available.names)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id # 各AZのパブリックサブネットに配置

  tags = {
    Name = "nat-gw-${data.aws_availability_zones.available.names[count.index]}"
  }

  # 依存関係の明示:インターネットゲートウェイの作成完了を待つ
  depends_on = [aws_internet_gateway.gw]
}

# 各プライベートサブネットに対応するAZのNATゲートウェイをルーティング
resource "aws_route_table" "private" {
  count  = length(data.aws_availability_zones.available.names)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id # ゾーンローカルなNATへルーティング
  }

  tags = {
    Name = "rt-private-${data.aws_availability_zones.available.names[count.index]}"
  }
}

この設計により、AZ-a のプライベートインスタンスからのトラフィックは、必ず同一AZ内の aws_nat_gateway.main[0] を通過し、クロスAZの通信コストとレイテンシーの増大を完全に排除する。万が一 AZ-a がダウンした際のマルチAZフェイルオーバー設計と組み合わせることで、堅牢性とパフォーマンスの双方を高次元で両立させることが可能となる。

—

結びにかえて

ネットワークの挙動は、決してブラックボックスではない。パケットは物理的な制約とカーネルのアルゴリズムに従って、極めてロジカルに目的地へとルーティングされている。

マルチAZ構成におけるNATゲートウェイの冗長化は、「ただ落ちないようにする」ためのものではない。それは、ゾーン障害というカオスの中でもパフォーマンスを維持し、コストの最適化とセキュアなトランスポート層の確立を同時に達成するための、インフラエンジニアの美学そのものである。

あなたの設計するネットワークは、次の障害を静かに、そして何事もなかったかのように乗り越えられるだろうか。今一度、ルートテーブルとカーネルのログに目を向けてみてほしい。

コメント

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