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

「NATゲートウェイは1つで十分」という慢心がシステムを殺す——マルチAZ冗長化の設計思想と実務の鉄則

こんにちは。現場で泥をかぶるSREの皆さん、今日もパケットの迷子に振り回されていますか?

クラウドインフラの設計において、「可用性」という言葉はあまりに使い古されていますが、いざNATゲートウェイ(NAT GW)の設計となると、途端に思考停止に陥るエンジニアを多く見かけます。「NAT GWを各AZに置くとコストがかかるから、とりあえず1つにしておこう」という判断。それが、あなたのシステムの「運命の分かれ道」になることを知っていますか?

今回は、単なる教科書的な構成図の話ではなく、実際にAZ障害が起きたとき、パケットがどう彷徨い、どのようにしてサービスを延命させるのか。その「泥臭い現実」を技術的に深掘りします。

なぜ「NAT GWのマルチAZ配置」が正義なのか

結論から言うと、NAT GWはAZごとのリソースです。AWSであれば、NAT GWを作成する際に特定のサブネットを指定しますよね。これはつまり、そのNAT GWがそのAZの物理的な計算機群に紐付いていることを意味します。

もし、AZ-aだけにNAT GWを置き、Web APIが稼働するPrivate Subnet(AZ-aとAZ-b)からそこにルーティングを向けていた場合、AZ-aで障害が発生したらどうなるでしょう?

1. AZ-aのNAT GWが沈黙する。
2. AZ-bのインスタンスからインターネットへの通信(外部API連携やパッケージ更新など)が全てタイムアウトする。
3. 負荷分散されているはずのWeb APIが、外部通信を伴う処理で次々と5xxエラーを吐き出す。

「AZ障害に強い構成」を目指してマルチAZにEC2やEKSを並べても、出口(NAT GW)がボトルネックになっていれば、それは単なる「高価なシングルポイント障害」に過ぎません。

ルートテーブルによるフェイルオーバーの仕組み

NAT GWを各AZ(AZ-a, AZ-b)に配置した場合、ルーティングは以下のように設計するのが現場の「定石」です。

  • Private Subnet-a (AZ-a) のルートテーブル: 0.0.0.0/0 -> nat-gw-a
  • Private Subnet-b (AZ-b) のルートテーブル: 0.0.0.0/0 -> nat-gw-b

ここで重要なのは、「クロスAZのNAT GW通信は避ける」という原則です。なぜなら、AZ間通信にはデータ転送料金が発生し、何よりレイテンシがわずかに悪化するからです。平常時は「自分のAZのNAT GWを使う」ことで、コストとパフォーマンスを最適化します。

AZ障害時の挙動:なぜ自動で切り替わらないのか

悲しい事実ですが、AWSやGCPのネイティブ機能として「NAT GWが死んだら自動で別のNAT GWにルートを書き換える」魔法はありません。

もしAZ-aが完全に消失した場合、AZ-a側のPrivate Subnetのルートテーブルを、AZ-bのNAT GWへ手動(またはEventBridge + Lambdaによる自動化)で書き換える必要があります。この「検知と復旧」の自動化こそが、真のSREの腕の見せ所です。

# AWS SDK (boto3) を用いたルートテーブル切り替えのイメージ
import boto3

def failover_nat_gateway(subnet_id, new_nat_gw_id):
    ec2 = boto3.client('ec2')
    # 該当サブネットに関連付けられたルートテーブルを特定し、ターゲットを更新
    ec2.replace_route(
        RouteTableId='rtb-xxxxxxxxxxxx',
        DestinationCidrBlock='0.0.0.0/0',
        NatGatewayId=new_nat_gw_id  # 障害のないAZのNAT GWを指定
    )
    print(f"Route updated to {new_nat_gw_id}")

実務で直面する「見えないタイムアウト」をデバッグする

NAT GWを冗長化していても、アプリケーション層で「なぜか通信が遅い/切れる」という相談を受けることがあります。これは大抵の場合、NAT GWのコネクション追跡(Conntrack)の制限や、アイドルタイムアウトが原因です。

外部のWeb API(例えば決済ゲートウェイなど)を叩く際、以下の curl コマンドのように、コネクションを再利用(Keep-Alive)させる設計になっているか確認してください。

# 明示的に接続を維持し、NAT GWのポート枯渇を防ぐ
curl -v -H "Connection: keep-alive" https://api.external-service.com/v1/data

また、Pythonの requests ライブラリを使用する場合も、セッションを使い回すことが必須です。

import requests

# セッションを使い回すことで、NAT GWのSNATポートを節約する
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10)
session.mount('https://', adapter)

response = session.get('https://api.external-service.com/v1/data')

デバッグの鉄則:パケットの「出口」を追え

もし通信障害が発生したら、まずは特定のAZのNAT GWを疑い、以下の手順で切り分けを行います。

1. VPCフローログの確認: REJECT されていないか。
2. SNATポートの枯渇確認: ErrorPortAllocation メトリクスをCloudWatchでチェック。
3. ルートテーブルの確認: aws ec2 describe-route-tables で、想定外の経路に飛ばされていないか確認。

最後に:ネットワークは「生き物」である

クラウドネットワークは、物理的な制約を隠蔽してくれますが、その上で動くアプリケーションは確実に物理的なAZ障害の影響を受けます。「NAT GWはAZごとに1つずつ」という構成は、少し贅沢に見えるかもしれません。しかし、真夜中の3時に「NAT GWが死んでサイト全体が共倒れした」という地獄のオンコールを経験したエンジニアなら、このコストが「生命保険」だということに同意してくれるはずです。

ネットワーク設計に「完璧」はありません。しかし、障害が起きたときに、どこでパケットが止まっているかを即座に特定し、手動でも自動でも復旧できる「道筋」を作っておくこと。それこそが、シニアエンジニアが持つべき矜持です。

皆さんのインフラが、今日も静かに、そして力強くパケットを運び続けますように。

コメント

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