NATゲートウェイは「贅沢に」使い倒せ:マルチAZ設計における可用性の極意
現場でIaC(TerraformやCDK)のコードレビューをしていると、コスト削減の名の下に「NATゲートウェイ(NAT GW)を1つのAZにしか配置していない」設計に遭遇することがあります。
正直に言います。その設計、数年後に必ず泣きを見ます。
クラウドの世界では「AZは独立した障害ドメインである」というのが大前提です。もしNAT GWを置いているAZがダウンしたら、そこを通っていた全パケットはロストし、あなたのWeb APIは即座に「外の世界(外部APIやDB、S3など)」と遮断されます。今日は、SREとして現場で培った「NAT GWの冗長化」における泥臭い知見を共有しましょう。
—
1. なぜ「NAT GW」をAZごとに独立させるのか
NAT GWは単なるルーターではありません。AWSであればマネージドサービスですが、裏側では特定のENI(Elastic Network Interface)を経由するステートフルな通信制御を行っています。
もし、AZ-aにしかNAT GWを置いていない状態でAZ-aが「死んだ」場合、AZ-bにいるあなたのサーバー群は、インターネットへ向かう出口を完全に失います。たとえALBがマルチAZで生きていても、バックエンドが外部APIを叩けなければ、Web APIは無力です。
冗長化の黄金律
「NAT GWはAZごとに独立して配置し、各AZのプライベートサブネットは、必ず『同じAZ内のNAT GW』へ向かうようにルーティングする」
これを守ることで、仮にAZ-aが物理的に消失しても、AZ-bの通信はAZ-bのNAT GWを通って安全に保護されます。
—
2. アーキテクチャの基本フロー
通信の流れを可視化してみましょう。
1. Request: プライベートサブネットのEC2/Fargateが、外部API(例: api.stripe.com)へリクエスト。
2. Routing: ルートテーブルが 0.0.0.0/0 を nat-gw-a(自分と同じAZ内のGW)へ転送。
3. SNAT: NAT GWが自身の持つ固定パブリックIPにソースIPを変換。
4. Response: 外部APIからのレスポンスは、NAT GWのENIを経由して元のPod/EC2へ帰還。
ここで重要なのは、「自分と同じAZ内のNAT GW以外を使ってはいけない」という点です。AZを跨いだ通信にはデータ転送コスト(AZ間転送費用)が発生しますし、何よりAZ障害時に道連れになるリスクが高まります。
—
3. 実践:IaCによる設計の実装(Terraform例)
TerraformでNAT GWを構築する際、冗長化を意識したコードは以下のようになります。各AZに対してリソースをループで回すのが定石です。
# 各AZに対応するNATゲートウェイを生成
resource "aws_nat_gateway" "nat" {
for_each = var.availability_zones
allocation_id = aws_eip.nat[each.key].id
subnet_id = aws_subnet.public[each.key].id # 必ずパブリックサブネットに配置
tags = { Name = "nat-gw-${each.key}" }
}
# AZごとのプライベートルートテーブルを設定
resource "aws_route_table" "private" {
for_each = var.availability_zones
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[each.key].id # 同じAZのNAT GWを指定
}
}
—
4. トラブルシューティング:接続が切れたらどこを見るか?
「APIがタイムアウトする」というアラートが上がったとき、SREがまず疑うべきはルートテーブルの不整合です。
デバッグコマンドの鉄板
まずは、コンテナ内やEC2内から traceroute や curl での挙動を追います。
# 外部APIへの疎通確認(verboseでヘッダーも確認)
curl -Iv https://api.example.com/v1/health
# パケットがどこで止まっているか確認
# 途中でタイムアウトする場合、NAT GWの経路が怪しい
traceroute -T -p 443 api.example.com
Pythonで確認する(コネクションタイムアウトの深掘り)
アプリケーションコード内で、特定のNAT GW経由であることを意識した疎通確認スクリプトを用意しておくと、障害時に即座に切り分けができます。
import requests
def check_external_connectivity():
url = "https://api.example.com/health"
try:
# タイムアウトを短めに設定して即座に異常を検知
response = requests.get(url, timeout=3)
print(f"Status Code: {response.status_code}")
except requests.exceptions.ConnectTimeout:
print("【障害】NAT GW経由の通信がタイムアウトしました。ルートテーブルを確認してください。")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
check_external_connectivity()
—
5. 運用上のTips:コストと可用性のバランス
「NAT GWを3つ作ると高すぎる!」という声が聞こえてきそうですが、ここが運用の腕の見せ所です。
- トラフィックの集約: 開発環境であれば、あえて冗長化を諦めて1つに集約する(コスト削減)。
- 本番環境の死守: 本番環境では最低2AZ構成を維持する。
- 監視の徹底: CloudWatchで
ErrorPortAllocationやBytesOutToDestinationを監視し、特定のNAT GWに負荷が集中していないかチェックしてください。
最後に
ネットワーク設計は、一度作ってしまうと修正が非常に困難です。NAT GWの冗長化は「保険」ではなく「インフラの基本設計」です。
パケットがどのゲートウェイを通って外の世界へ羽ばたき、どう戻ってくるのか。そのパスを常に頭の中で想像し、AZ障害という最悪のシナリオを想定して設計する。これこそが、信頼されるSREへの第一歩です。
皆さんのインフラが、どんな障害にも負けない強靭なものになることを願っています。次は、このNAT GWのボトルネックをどう可視化するかについて語りましょうか。それでは!
コメント