「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が死んでサイト全体が共倒れした」という地獄のオンコールを経験したエンジニアなら、このコストが「生命保険」だということに同意してくれるはずです。
ネットワーク設計に「完璧」はありません。しかし、障害が起きたときに、どこでパケットが止まっているかを即座に特定し、手動でも自動でも復旧できる「道筋」を作っておくこと。それこそが、シニアエンジニアが持つべき矜持です。
皆さんのインフラが、今日も静かに、そして力強くパケットを運び続けますように。
コメント