【AWS SRE現場発】NATゲートウェイの「見えないマルチAZ挙動」とクロスAZトラフィックの罠:障害に強いVPC設計の極意
こんにちは。数々の修羅場(夜間障害や原因不明のパケットロス)をくぐり抜けてきたシニアSREの私です。
クラウドインフラの設計で、最も油断が生まれやすいポイントの一つが「VPCのインターネット接続」です。特に、プライベートサブネットから外部のWeb APIやサードパーティサービスを叩く際に必須となるNATゲートウェイ(NAT Gateway)。
「とりあえず各AZ(アベイラビリティゾーン)に1つずつ配置しておけばマルチAZで冗長化されて安心!」――そう考えていませんか?
もちろん、アーキテクチャ図としては正解です。しかし、その裏でパケットがどのようにルーティングされ、AZ障害時にAWSの制御プレーンがどう動くのか、そして「知らず知らずのうちに膨れ上がるクロスAZデータ転送コスト」の正体を正確に理解しているエンジニアは、意外と多くありません。
今回は、実務でWeb API設計やインフラ運用に携わるエンジニアの皆さんに向けて、NATゲートウェイのマルチAZ構成におけるリアルな挙動、自動フェイルオーバーの仕組み、そして現場で使えるデバッグの知見を徹底解説します。
—
1. NATゲートウェイのマルチAZ構成と「見えないフェイルオーバー」
まず前提として、AWSのNATゲートウェイは「マネージドサービス」です。内部で動いている冗長化されたEC2インスタンスやENI(Elastic Network Interface)の群れは、私たちユーザーからはブラックボックス化されています。
マルチAZ構成を組む場合、標準的なベストプラクティスは以下の通りです。
1. AZ-a、AZ-cなどの各プライベートサブネットに対応するNATゲートウェイをそれぞれ配置する。
2. 各AZのプライベートサブネット用のルートテーブルで、0.0.0.0/0 の宛先を「そのAZにあるNATゲートウェイ」に向ける。
AZ間トラフィックの原則と自動フェイルオーバー
ここで重要な原則があります。「NATゲートウェイは、原則として作成されたAZ内で完結する通信を処理する」ということです。
もし、AZ-aのプライベートサブネットにあるアプリサーバーから外部APIを叩く場合、パケットはAZ-aのNATゲートウェイを経由してインターネットへ出ていきます。このとき、クロスAZ(AZ間)のデータ転送は発生しません。
では、AZ-aのNATゲートウェイが物理的な障害などでダウンした場合、何が起きるのでしょうか?
- 自動フェイルオーバーは起きない(ルートテーブルの観点):
AZ-aのルートテーブルのデフォルトルートは、あくまで「AZ-aのNATゲートウェイ」を指しています。AWSは勝手にルートテーブルを書き換えて、生きているAZ-cのNATゲートウェイへトラフィックを逃がしてはくれません。ルートテーブルは静的な設定だからです。
- では、どうやって冗長化するのか?:
真のマルチAZ冗長化を実現するには、AWS Fault Injection Simulator (FIS) やRoute 53、あるいはアプリケーションレイヤー、あるいは複数のNATゲートウェイへのルートを動的に制御する高度なルーティング(Transit Gatewayの活用など)を考慮する必要がありますが、一般的なVPC単体構成では「各AZのプライベートサブネットは、自AZのNATゲートウェイが死んだらインターネット接続が失われる(フェイルオープンしない)」という挙動になります。
したがって、真の意味で可用性を担保するには、「アプリケーション自体がマルチAZで稼働しており、各AZのインスタンスがそれぞれのAZにあるNATゲートウェイを正しく踏んでいるか」を確認し続ける必要があります。
—
2. 誰もがハマる「クロスAZトラフィックコスト」の罠
マルチAZにNATゲートウェイを置いた際、SREが月末のAWS請求書を見て冷や汗をかく原因ナンバーワンが、「クロスAZデータ転送量(Cross-AZ Data Transfer)」です。
なぜコストが発生するのか?
「各AZにNATゲートウェイを置いたんだから、トラフィックはローカルで完結しているはず」と思い込んでいませんか? 以下のケースで意図せぬクロスAZ通信が発生します。
1. パブリックサブネットとプライベートサブネットのAZ不一致
NATゲートウェイ自体は「パブリックサブネット」に配置し、Elastic IP(EIP)をアタッチします。もし、AZ-aのプライベートサブネットからのパケットが、設計ミスやルートテーブルのルーティング重複により、AZ-cに置いたNATゲートウェイへ向かってしまった場合、プライベートサブネット(AZ-a) ➔ NATゲートウェイ(AZ-c) の間でクロスAZ通信が発生します。
2. トラフィックの非対称性とアプリケーションの偏り
例えば、バッチ処理や重いWeb APIリクエストを処理するワーカーが特定のAZ(例:AZ-a)に集中してスケールアウトしたとします。AZ-aのNATゲートウェイに負荷が集中し、AWS側で自動スケール(NATゲートウェイは裏で帯域に応じてスケールしますが、急激なトラフィック増にはタイムラグがあります)する過程で、あるいはトラフィックの集約によって、予期せぬコストハネが発生します。
—
3. 実践:Terraformによる冗長化されたNATゲートウェイの構築例
実務でそのまま使える、冗長性を考慮したVPCおよびNATゲートウェイのTerraformコードスニペットです。各AZに独立したNATゲートウェイを配置し、ルートテーブルを正しく分離しています。
# -----------------------------------------------------------------
# 変数定義
# -----------------------------------------------------------------
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
variable "azs" {
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
# -----------------------------------------------------------------
# VPC & インターネットゲートウェイ
# -----------------------------------------------------------------
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "production-vpc"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "production-igw"
}
}
# -----------------------------------------------------------------
# パブリックサブネット & NATゲートウェイ (AZごとに作成)
# -----------------------------------------------------------------
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) # 10.0.0.0/24, 10.0.1.0/24
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${var.azs[count.index]}"
}
}
# 各NATゲートウェイ用のEIP
resource "aws_eip" "nat" {
count = length(var.azs)
domain = "vpc"
tags = {
Name = "nat-eip-${var.azs[count.index]}"
}
depends_on = [aws_internet_gateway.igw]
}
# NATゲートウェイの配置(各AZのパブリックサブネットへ)
resource "aws_nat_gateway" "nat" {
count = length(var.azs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "nat-gw-${var.azs[count.index]}"
}
depends_on = [aws_internet_gateway.igw]
}
# -----------------------------------------------------------------
# プライベートサブネット & 独立したルートテーブル
# -----------------------------------------------------------------
resource "aws_subnet" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10) # 10.0.10.0/24, 10.0.11.0/24
availability_zone = var.azs[count.index]
tags = {
Name = "private-subnet-${var.azs[count.index]}"
}
}
resource "aws_route_table" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
# 自AZのNATゲートウェイへトラフィックをルーティング(クロスAZコストを防ぐ)
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[count.index].id
}
tags = {
Name = "private-rt-${var.azs[count.index]}"
}
}
# プライベートサブネットとルートテーブルの紐付け
resource "aws_route_table_association" "private" {
count = length(var.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
—
4. 現場のトラブルシューティング:外部API接続断のデバッグ手法
プライベートサブネット上のアプリケーションから外部APIへの疎通が突然途絶えたとき、SREが現場でどのような手順で原因を切り分けるのか、その実務的なフローを公開します。
ステップ1: パケットの脱出経路(ルーティング)の確認
まずは、問題が発生しているインスタンスがどのプライベートサブネットに属しているかを確認し、該当するルートテーブルが正しいNATゲートウェイを向いているかをAWS CLIで確認します。
# 特定のサブネットに関連付けられているルートテーブルIDを調べる
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-0123456789abcdef0" \
--query "RouteTables[*].[RouteTableId, Routes[*]]" \
--output json
ステップ2: アプリケーションからの疎通確認とIPアドレスの特定
コンテナ(ECS/EKS)やEC2のシェルに入り、実際にどのNATゲートウェイのIP(EIP)経由で外に出ていっているかを確認します。 https://ifconfig.me などのサービスをcurlで叩くのが手っ取り早いです。
# プライベートサブネット上のインスタンスから外部への出口IPを確認する
curl -s https://ifconfig.me
*もし期待するAZのNATゲートウェイのEIPとは異なるIPが返ってきた場合、ルートテーブルの誤設定や、ロードバランサー・プロキシを経由していることによるルーティングのねじれが疑われます。*
ステップ3: VPCフローログ(VPC Flow Logs)によるパケット解析
「パケットがどこでドロップしているのか?」を突き止めるには、VPCフローログが最強の武器です。特に REJECT ステータスや、送信先ポートへのトラフィックが記録されているかを確認します。
-- Amazon AthenaでVPCフローログをクエリし、外部への通信ドロップを調査する例
SELECT
day,
srcaddr,
dstaddr,
dstport,
protocol,
action,
packets,
bytes
FROM
vpc_flow_logs.flow_log_table
WHERE
action = 'REJECT'
AND start >= to_unixtime(timestamp '2023-10-01 00:00:00')
ORDER BY
bytes DESC
LIMIT 20;
—
5. まとめ:シニアSREからの実務的アドバイス
NATゲートウェイのマルチAZ構成は、単に「各AZにリソースを並べる」だけでは不十分です。
1. ルートテーブルは必ずAZごとに分離し、自AZのNATゲートウェイを指すように厳格に管理する(クロスAZデータ転送コストの抑制と、AZ障害時の影響範囲の局所化)。
2. 「マルチAZ=全自動で別AZへフェイルオーバーする」という誤解を捨てる。VPCのルートテーブルは静的であるため、アプリ側の可用性や外形監視、万が一の際のルート切替手順(IaCの迅速な適用など)を頭に入れておく。
3. VPCフローログやCloudWatch Metrics(ErrorPortAllocation, BytesOutToDestination 等)を監視メトリクスに組み込み、異常なトラフィックの偏りを早期に検知する体制を作る。
クラウドのネットワークは目に見えないからこそ、基礎的なパケットの流れる原理原則に立ち返ることが、障害を未然に防ぐ最大の防御壁となります。皆さんのインフラ設計の一助になれば幸いです。
コメント