はじめに:その「ケチったNAT設計」、月末のAWS請求書で泣きを見ますよ
こんにちは。SREとして数々のクラウドインフラの設計・構築、そして修羅場のような障害対応をくぐり抜けてきたシニアエンジニアです。
Web APIの設計やインフラのサイジングをしているとき、どうしてもコスト削減の魔力に引きずり込まれる瞬間がありますよね。「プライベートサブネットから外部APIやSaaSへ通信するためのNATゲートウェイ。AZごとに置くと毎月それなりの固定費がかかるから、いっそ1つのAZにまとめて全AZで共有すればいいのでは?」――お気持ちは痛いほど分かります。私も若かりし頃、同じ罠にハマりかけました。
しかし、ちょっと待ってください。その「単一NATゲートウェイへの集約」、本当にコスト削減になっていますか?
今回は、AWSなどのパブリッククラウドにおけるクロスAZ(アベイラビリティゾーン)NATゲートウェイ配置時のデータ転送コストと高可用性(HA)のジレンマについて、パケットの実際の挙動や泥臭い現場の知見を交えて徹底解説します。月末のAWS請求書を見て青ざめる前に、一緒に正しいネットワーク設計の勘所を押さえましょう。
—
1. なぜ「単一NAT共有」は危険なのか?:パケットの旅路とコストの正体
まずは、マルチAZ構成のプライベートサブネットから、単一のAZに配置されたNATゲートウェイへ通信が集約されるときのデータフローを頭に思い浮かべてみてください。
例えば、ap-northeast-1a(東京リージョンAZ A)にNATゲートウェイを1台だけ置き、隣の ap-northeast-1c(AZ C)にあるプライベートサブネット内のECSタスクやEC2インスタンスから、外部の決済APIへ POST リクエストを送るとしましょう。
クロスAZ通信が発生するメカニズム
1. アプリからの発信: AZ Cのコンテナが、外部APIに向けてパケットを送信します。
2. ルーティング: プライベートサブネットのルートテーブルには「0.0.0.0/0 への宛先は nat-xxxx(AZ AにあるNAT)」と記述されています。
3. AZ間移動: パケットはAWSの基盤ネットワーク(物理的なバックボーン)を通り、AZ CからAZ Aへと転送されます。ここで「クロスAZデータ転送」が発生します。
4. NAT変換: AZ AのNATゲートウェイがプライベートIPをパブリックIPに変換し、インターネット上の外部APIへ向けてパケットを送り出します。
5. レスポンスの返却: 外部APIからのレスポンスも同様にAZ AのNATに返り、再びAZ間を跨いでAZ Cのコンテナへと戻ります。
恐るべき「AZ間データ転送コスト」
AWSの場合、異なるAZ間を行き来するデータ(Cross-AZ Data Transfer)には、1GBあたり数セント(おおむねイン・アウト合算で $0.02/GB 前後)の費用がかかります。
「たった数セントでしょ?」と侮ってはいけません。
例えば、画像や動画を頻繁にやり取りするメディア系APIや、外部のビッグデータ基盤に数TB規模のJSONペイロードを投げ続けるマイクロサービスを想像してください。月間10TBの通信を行えば、AZ間転送費用だけで毎月数百ドルの「見えない税金」が基本のNATゲートウェイ料金に上乗せされます。これが1年、2年と積み重なると……経営陣から詰められる理由としては十分すぎる金額になりますよね。
—
2. 高可用性(HA)の観点:シングルポイント・オブ・ディフェンス(SPOF)の恐怖
コスト以上に恐ろしいのが、可用性の低下(SPOFの誕生)です。
もし、単一のNATゲートウェイを配置しているAZ Aで、物理的な障害やネットワークの断絶が発生したらどうなるでしょうか?
他のAZ(AZ CやAZ D)で元気に稼働しているアプリケーション群は無事なはずなのに、外部APIとの通信が突如としてすべて途絶えます。「DBは生きているのに、外部SaaSと連携する決済や認証処理だけが全滅した」という、SREにとって悪夢のようなインシデントの完成です。
クラウドの設計原則である「マルチAZ冗長化」に反した構成をとることは、システムのレジリエンス(復元力)を自らドブに捨てるようなものです。
—
3. 解決策:各AZへのNATゲートウェイ分散配置パターン
では、このジレンマをどう解決すべきでしょうか。答えはシンプルです。「各AZにNATゲートウェイを1台ずつ配置し、ルートテーブルをAZごとに独立させる」という王道パターンです。
アーキテクチャの基本方針
- AZ Aのプライベートサブネット ➔ ルートテーブルA ➔ AZ AのNATゲートウェイ ➔ インターネット
- AZ Cのプライベートサブネット ➔ ルートテーブルC ➔ AZ CのNATゲートウェイ ➔ インターネット
この構成にすることで、以下のような劇的なメリットが生まれます。
1. クロスAZデータ転送のゼロ化: 原則として、同一AZ内でトラフィックが完結するため、AZ間データ転送コストを最小限(ほぼゼロ)に抑えられます。
2. 真の高可用性: 仮にAZ Aが被災しても、AZ CのNATゲートウェイは影響を受けず、トラフィックのルーティングを維持できます(ただし、アプリ側もマルチAZで冗長化されていることが前提です)。
—
4. 実務で役立つ設定・実装サンプル
ここからは、より具体的に実務でどう表現されるのかを見ていきましょう。TerraformなどのIaC(Infrastructure as Code)や、実際のアプリケーション側でのタイムアウト設計のコツをご紹介します。
TerraformによるマルチAZ NAT配置のイメージ
インフラをコードで管理する際、リソースをループさせて各AZに綺麗にNATを配置するのはモダンなSREの常套句です。
# 変数で定義されたAZのリストに対して、サブネットとNATゲートウェイを動的に作成
variable "availability_zones" {
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
# パブリックサブネット(NAT用)の作成
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "public-subnet-${var.availability_zones[count.index]}"
}
}
# 各パブリックサブネットに紐づくEIP
resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"
tags = {
Name = "nat-eip-${var.availability_zones[count.index]}"
}
}
# 各AZにNATゲートウェイを配置
resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "nat-gw-${var.availability_zones[count.index]}"
}
}
アプリケーション側(Python / requests)での堅牢なタイムアウト設計
ネットワークトポロジーが変わると、パケットの経路長やNAT機器のセッションテーブルの挙動も微妙に変わります。特に 외부 API と通信する際は、必ず適切なタイムアウトを設定してください。
import requests
from requests.exceptions import Timeout, RequestException
def call_external_api(payload):
url = "https://api.example.com/v1/data"
# コネクションプールやタイムアウトを明示的に指定し、
# ネットワークの一時的な瞬断やNATのコネクション枯渇に備える
timeout_config = (3.0, 10.0) # (接続確立時のタイムアウト, 読み込み時のタイムアウト)
try:
response = requests.post(url, json=payload, timeout=timeout_config)
response.raise_for_status()
return response.json()
except Timeout:
# ログに詳細を吐き出し、サーキットブレーカー等へ連携する文脈
print("Error: External API request timed out. Check NAT/Network route.")
raise
except RequestException as e:
print(f"Error: Network or HTTP error occurred: {e}")
raise
—
5. 現場のシニアが教える「陥りがちな罠」とデバッグTips
最後に、現場で実際に遭遇しがちなトラブルシューティングのノウハウをいくつかシェアしておきます。
1. 「SNATポート枯渇」への対策
各AZにNATを分散させると、それぞれのNATゲートウェイが持つパブリックIPあたりのポート数(最大64,000ポート)が、そのAZに偏ったトラフィックによって枯渇することがあります。
もし特定のAZにトラフィックが集中しやすいバッチ処理などがある場合は、aws_nat_gateway に複数のEIPを割り当てる(パブリックIPv4アドレスの追加)アプローチを検討してください。
2. トラブルシューティングの鉄則:フローログを見ろ
「なぜか外部APIへの通信がパケットロスする」という不可解な現象にぶ1つあたったら、迷わず VPCフローログ(VPC Flow Logs) を有効化してください。
CloudWatch LogsやAmazon Athenaに流し込み、REJECT されているトラフィックや、どのNATゲートウェイを経由しているのかのルーティングをパケット単位で追跡するのが、問題解決への一番の近道です。勘や推測でインフラをいじると、大抵沼にハマります。
—
おわりに
「NATゲートウェイを1つにしてコストを削る」というアプローチは、初期の小さなおもちゃのようなシステムであれば通用するかもしれません。しかし、ビジネスの成長とともにスケールしていくWeb APIやミッションクリティカルなシステムにおいて、この設計は時限爆弾になり得ます。
- 短期的・見かけ上のコスト ではなく、長期的・AZ間転送費用および障害時のダウンタイムコスト をトータルで比較すること。
- クラウドの基本思想である「マルチAZ冗長化」に逆らわない設計を選ぶこと。
この記事が、あなたの次のインフラ設計やコードレビューの際の良い羅針盤となれば幸いです。それでは、また次回の現場の知見でお会いしましょう!
コメント