【実務・中級編】 AWS VPCにおけるNATゲートウェイの冗長性とAZ間フェイルオーバーの挙動 – クラウドインフラと仮想化ネットワーク実践ガイド

【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 等)を監視メトリクスに組み込み、異常なトラフィックの偏りを早期に検知する体制を作る。

クラウドのネットワークは目に見えないからこそ、基礎的なパケットの流れる原理原則に立ち返ることが、障害を未然に防ぐ最大の防御壁となります。皆さんのインフラ設計の一助になれば幸いです。

コメント

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