マルチAZ時代におけるNATゲートウェイの「真の冗長化」:ゾーン障害を生き抜くパケットルーティングとカーネルチューニングの極意
クラウドネイティブなインフラストラクチャにおいて、可用性は信仰ではなく数学的な確率計算に基づいている。しかし、多くのシステム設計レビューにおいて、パブリッククラウドの「マネージドサービスだから安全」という甘い神話が、いまだに根深く残っているのも事実だ。
その代表格が、AWSのNATゲートウェイ(あるいはGCPのCloud NAT)の配置設計である。単一のNATゲートウェイをプライベートサブネットのデフォルトルートに据え、「マネージドだから裏で勝手に冗長化されているだろう」と高を括っていはいないだろうか。
結論から言えば、クラウドプロバイダが提供するマネージドNATは単一AZ(Availability Zone)のリソースである。ひとたびそのAZで物理的な電源断や重大なハードウェア障害が発生すれば、そこに依存したプライベートサブネットからの外向き通信は一巻の終わりだ。
今回は、真のゾーン障害耐性を手に入れるための「マルチAZ独立配置型NATゲートウェイ」のアーキテクチャと、パケットレベルの挙動、そして極限のパフォーマンスを引き出すためのLinuxカーネルおよびTCP/IPチューニングの深淵を紐解いていく。
—
1. ゾーン独立型NATゲートウェイとルートテーブルの数学的・動的設計
可用性を担保するための基本方針は極めてシンプルである。「すべてのAZに、そのAZ専用のNATゲートウェイを配置し、プライベートサブネットのルートテーブルもAZごとに独立させる」。
これの何が優れているかといえば、レイテンシの最適化と障害ドメインの完全な分離だ。
[ AZ-a ]
├─ Public Subnet ── ( NAT-GW-a ) ── [ Internet ]
└─ Private Subnet ── ( Route Table a: 0.0.0.0/0 -> NAT-GW-a )
[ AZ-c ]
├─ Public Subnet ── ( NAT-GW-c ) ── [ Internet ]
└─ Private Subnet ── ( Route Table c: 0.0.0.0/0 -> NAT-GW-c )
もし AZ-a が完全に沈黙したとしても、AZ-c に配置されたワークロードは、自身のAZ内にある NAT-GW-c を経由して外部との通信を継続できる。クロスAZ間のトラフィック転送が発生しないため、AWSであればデータ転送コスト(Cross-AZ Data Transfer Fee)の削減にも直結する。
ゾーン障害時の挙動:パケットはどこを流れるのか?
ここで重要になるのが、Kubernetesクラスターのノード(Node)やPodが配置されるAZと、NATゲートウェイのトラフィックパスの関係性だ。
AWSのEKSや自前で構築したKubernetes環境において、Podから外部APIへのリクエストが発生した際、パケットは次のような経路をたどる。
1. Podが所属するWorker NodeのLinuxカーネルが、宛先IPへのルーティングテーブルを評価する。
2. デフォルトルート (0.0.0.0/0) に従い、パケットはホストの eth0 からVPCルーターへ送出される。
3. VPCルーターは、パケット送信元IPアドレスが属するサブネットのルートテーブルを参照し、同一AZ内に存在するNATゲートウェイへパケットを転送する(SNAT処理)。
ここで AZ-a のNATゲートウェイがダウンした場合、AZ-a 内のプライベートサブネットにあるルートテーブルのターゲットが無効化される。この状態を自動検知し、API経由でルートテーブルの 0.0.0.0/0 を AZ-c のNATゲートウェイへ動的に書き換える仕組み(あるいは最初からAZローカルなルーティングを厳密に強制する設計)が不可欠となる。
Terraformを用いて、このマルチAZ独立型のルーティング基盤を構築する際の実装例を以下に示す。
# AZ-a 用のパブリックサブネットとNATゲートウェイ
resource "aws_subnet "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
tags = { Name = "public-subnet-a" }
}
resource "aws_eip "nat_a" {
domain = "vpc"
depends_on = [aws_internet_gateway.igw]
}
resource "aws_nat_gateway "nat_gw_a" {
allocation_id = aws_eip.nat_a.id
subnet_id = aws_subnet.public_a.id
tags = { Name = "nat-gateway-a" }
}
# AZ-a プライベートサブネット用ルートテーブル(AZ-aのNATへ向ける)
resource "aws_route_table "private_a" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gw_a.id
}
tags = { Name = "private-rt-a" }
}
resource "aws_route_table_association "private_a_assoc" {
subnet_id = aws_subnet.private_a.id
route_table_id = aws_route_table.private_a.id
}
—
2. SNATの裏側:ポート枯渇とコネクション追跡の闇
NATゲートウェイの本質は、プライベートIPアドレスをパブリックIPアドレスに変換する NAPT (Network Address Port Translation) である。ここでエンジニアを絶望の淵に追い込むのが、「ポート枯渇 (Port Exhaustion)」 という物理的制約だ。
一つのパブリックIPアドレスが持つTCP/UDPのポート数は 65,535 個だが、そのうちシステム予約ポートを除くと、実際に利用できるエフェメラルポート(動的ポート)は約 64,000 個程度に制限される。
NATゲートウェイは、以下の4要素(5タプルの一部)の組み合わせでコネクションを識別している。
[NATのパブリックIP] : [NATの送信元ポート] <---> [宛先IP] : [宛先ポート]
同一の宛先サーバー(例えば外部の決済APIなど)に対して、短時間に数万を超えるリクエストを並列発火させると、あっという間に同一宛先へのポート割り当て上限に達し、接続エラー(EAGAIN: Resource temporarily unavailable やタイムアウト)が頻発する。
接続多重化とKeep-Aliveの最適化
この問題を回避するためには、アプリケーション層でのコネクションプーリングと、HTTP/1.1の Keep-Alive または HTTP/2 の多重化が必須となる。毎回新しいTCPコネクションを張るような実装(HTTP/1.0の挙動や、毎回 curl を新規実行するようなバッチ処理)は、マルチAZであっても一瞬でNATを窒息させる。
さらに、Linuxカーネルのネットワークスタック側でも、TIME_WAIT状態のソケットを効率的に再利用するためのチューニングが不可欠となる。以下のカーネルパラメータ (/etc/sysctl.conf) をコンテナノードに適用し、ポートの回転率を極限まで高めるべきだ。
# TIME_WAIT状態のソケットを迅速に再利用(安全な範囲でTCPタイムスタンプと併用)
net.ipv4.tcp_tw_reuse = 1
# エフェメラルポートの範囲を拡張し、利用可能なポート数を最大化
net.ipv4.ip_local_port_range = 1024 65535
# TCPのFIN-WAIT-2タイムアウトを短縮し、ゾンビコネクションの早期解放
net.ipv4.tcp_fin_timeout = 15
# SYNパケットに対するSYNACKの再送回数を減らし、異常な接続試行を早期切断
net.ipv4.tcp_synack_retries = 2
—
3. ゾーン障害発生時のレイテンシ急増とTCP再送(Retransmission)のメカニズム
もし、稼働中の AZ-a でネットワークの断絶や深刻なパケットロスが発生した場合、何が起きるのか。
クライアント(Pod)から外部サービスへのパケットは AZ-a のNATゲートウェイに向かうが、パケットは途中でブラックホールに吸い込まれたように消失する。このとき、TCPの信頼性レイヤーは次のような挙動を示す。
1. ACKの未受信: 送信側(Pod)は、指定されたRTO(Retransmission Timeout)の期間内にACKを受け取れない。
2. Exponential Backoff(指数バックオフ): カーネルはパケットのロスを検知し、RTOを2倍、4倍へと徐々に延ばしながら再送を繰り返す。
3. アプリケーションのレイテンシスパイク: 再送が収束するまでの数秒間、スレッドやコネクションがブロックされ、APIのレスポンスタイムが跳ね上がる。
ここで、KubernetesのLiveness/Readinessプローブや、サーキットブレーカー(EnvoyやIstioなどのサービスメッシュ)が迅速に反応しないと、連鎖的な障害(Cascading Failure)へと発展する。
ゾーン切替時のTLSコネクションへの影響
マルチAZのルートテーブルを動的に切り替えたとしても、既存の確立済みTCPコネクション(Established State)はどうなるのだろうか?
答えは「切断される」である。
NATゲートウェイが変わるということは、外向きのパブリックIPアドレス(あるいはNAT内部のステートフルなコネクションテーブル)が完全にリセットされることを意味する。新しいNATゲートウェイには、古いパケットの文脈(シーケンス番号やNATマッピング)が存在しないため、切り替え瞬間に既存のTCPセッションはRSTパケットによって強制終了される。
したがって、真のゾーン障害耐性を実現するアーキテクチャでは、インフラ層の冗長化だけに頼るのではなく、アプリケーション層で 「コネクション切断を検知し、即座にリトライ(冪等性の担保された再接続)を行うロジック」 が絶対に必要となる。
—
4. セキュリティとパフォーマンスの両立:TLSハンドシェイクの最適化
マルチAZ構成において、パケットが異なるAZのNATゲートウェイを経由する場合や、パブリックインターネットへ抜ける際のセキュリティとレイテンシのトレードオフについても触れておこう。
外部APIとの通信において、トランスポート層の暗号化(TLS 1.3)は必須である。しかし、ゾーン障害による経路変更や、遠隔地のエンドポイントとの通信では、RTT(Round Trip Time)の増大がスループットを大きく制限する。
1. TCP Window Scalingの有効化
高帯域・高遅延なパブリックネットワークにおいて、デフォルトのTCPウィンドウサイズではパイプラインを満たせない。以下のカーネルパラメータでウィンドウのスケーリングを確実に有効化する。
# TCPウィンドウのスケーリングを有効化し、広大なBDP(Bandwidth-Delay Product)に対応
net.ipv4.tcp_window_scaling = 1
# 最大TCPソケットバッファサイズを拡大
net.ipv4.core.rmem_max = 16777216
net.ipv4.core.wmem_max = 16777216
2. TLS 1.3 0-RTT(Zero Round Trip Time Resumption)の活用
可能であれば、外部APIクライアントの実装においてTLS 1.3の0-RTT機能を有効化し、ハンドシェイクのオーバーヘッドを削減する。ただし、0-RTTデータはリプレイ攻撃に対して脆弱であるため、対象のリクエストが完全に「冪等(Idempotent)」である場合(例: GETリクエストや特定の安全なAPIコール)に限定して適用すべきだ。
—
5. まとめ:現場のSREが取るべき「次の一手」
パブリッククラウドのマネージドサービスは強力だが、「設定すれば自動で無敵になる魔法の箱」ではない。
1. AZごとの独立したNATゲートウェイと専用ルートテーブルの徹底(クロスAZ依存の排除)
2. エフェメラルポート枯渇を防ぐためのカーネルチューニングとコネクション管理
3. ゾーン障害・ルート切替時に既存TCPコネクションが切断される前提のアプリケーション設計(冪等性とリトライ戦略)
これらをコード(IaC)とカーネルパラメータのレベルで泥臭く統制して初めて、真のゾーン障害耐性を持つモダンなクラウドインフラストラクチャが完成する。
障害はいつか必ず起きる。パケットがどの経路をたどるのか、その一本の道筋を頭の中で完全にビジュアライズできるかどうかが、プロのインフラエンジニアと単なる設定代行者の分かれ道なのだ。
コメント