こんにちは。現場のSREチームで日々インフラの底上げと格闘しているシニアエンジニアです。
夜中に「突然外部の決済APIと通信できなくなった!」というアラートが鳴り響き、冷や汗をかいた経験はありませんか? 原因を調査すると、プライベートサブネットからインターネットへの出口である「単一のNATゲートウェイ」が配置されていたAZ(アベイラビリティゾーン)で、まさかの基盤障害が発生していた――これはクラウドインフラ運用者にとって最悪の悪夢の一つです。
クラウドの旨味を最大限に引き出し、止まらないシステムを作り上げるためには、ネットワークの足回りから「マルチAZの思想」を徹底する必要があります。今回は、パブリック/プライベートサブネットの基本構造のおさらいから、可用性ゾーン(AZ)障害耐性を完璧に担保する「マルチAZ NATゲートウェイ配置設計」の極意を、現場のリアルな知見と共にお届けします。
—
1. なぜ「1つのNAT Gateway」では夜も眠れなくなるのか
近代的なクラウド(AWSのVPCやGCPのVPCなど)において、インターネット非公開のプライベートサブネット(データベースや内部マイクロサービスが稼働する領域)から外部のWeb APIやSaaSへ安全にアクセスするためには、SNAT(Source Network Address Translation)を行うコンポーネントが不可欠です。AWSを例に取れば、それが NAT Gateway です。
ここで多くの設計初心者が陥る罠が、コスト削減や「なんとなく動くから」という理由で、コストの安い単一のAZにのみ NAT Gateway を1つだけ配置してしまう構成です。
[Internet]
│
▼
[Public Subnet (AZ-a)] ── (NAT Gateway)
▲
│ (ルートテーブルの向き先が単一)
[Private Subnet (AZ-a)] [Private Subnet (AZ-c)]
(アプリA) (アプリB ※AZ-cの障害時に完全孤立)
この「シングルNAT構成」の恐ろしさは、単一のAZ障害(電源系統のトラブルや物理ホストの致命的なハードウェア障害など)が発生した瞬間、そのAZ内だけでなく、別の健常なAZ(例えば AZ-c)で動いているプライベートサブネットのリソースからも、インターネットへの外向き通信が一切できなくなるという「隠れた単一障害点(SPOF)」を生み出してしまう点にあります。
RFC 1916や各クラウドのベストプラクティスが口を酸っぱくして「マルチAZ配置」を推奨する理由はここにあります。アプリケーションサーバーをいくらオートスケーリングでマルチAZ冗長化しても、出口のネットワークが1箇所であれば、システム全体としての可用性は「出口の可用性」に引きずられてしまうのです。
—
2. マルチAZ NATゲートウェイ配置設計の全体像
真の高可用性を実現するアーキテクチャでは、利用するすべてのAZ(最低2つ、できれば3つ)に対して、それぞれ独立した NAT Gateway を配置し、プライベートサブネット側のルートテーブルもAZごとに分離・最適化します。
通信フロー(シーケンス)の基本原則
プライベートサブネットから外部APIへのリクエストが飛ぶとき、ネットワークのパケットは次のような厳密な「ローカリティ(局所性)」の原則に従ってルーティングされます。
1. プライベートサブネット (AZ-a) のアプリケーションが fetch() や curl で外部APIを叩く。
2. 同一AZ (AZ-a) 内のルートテーブルの定義に従い、トラフィックは NAT Gateway (AZ-a) へ転送される。
3. NAT Gateway (AZ-a) は、自身のパブリックIPアドレスに送信元IP(SNAT)を書き換えて、インターネットへ送出する。
4. 外部APIからのレスポンスは NAT Gateway (AZ-a) に戻り、プライベート側の元のコンテナへ返される。
もし AZ-a がブラックアウトしたとしても、AZ-c にあるアプリケーションは AZ-c のルートテーブルと NAT Gateway (AZ-c) を通じて何事もなかったかのように通信を継続できます。
—
3. 実践:TerraformによるマルチAZ NATゲートウェイの構築コード
口で言うのは簡単ですが、実際にインフラをコード(IaC)として美しく実装するにはどうすればよいでしょうか。以下に、AWSのTerraformを例にした、マルチAZ対応の堅牢なネットワーク構成のサンプルコードを提示します。
# ----------------------------------------------------------------------
# 変数の定義(可用性を担保するため最低2つのAZを指定)
# ----------------------------------------------------------------------
variable "azs" {
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
# ----------------------------------------------------------------------
# Elastic IP (EIP) の作成:各NAT Gatewayに固定グローバルIPを付与
# ----------------------------------------------------------------------
resource "aws_eip" "nat" {
count = length(var.azs)
domain = "vpc"
tags = {
Name = "nat-gateway-eip-${var.azs[count.index]}"
}
}
# ----------------------------------------------------------------------
# パブリックサブネットの作成(各AZに1つずつ)
# ----------------------------------------------------------------------
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${var.azs[count.index]}"
}
}
# ----------------------------------------------------------------------
# NATゲートウェイの作成(各AZに独立して配置する高可用性デザイン)
# ----------------------------------------------------------------------
resource "aws_nat_gateway" "main" {
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]}"
}
# パブリックサブネット作成完了後にNAT Gatewayを作る依存関係を明示
depends_on = [aws_internet_gateway.gw]
}
# ----------------------------------------------------------------------
# プライベートサブネットの作成(各AZに1つずつ)
# ----------------------------------------------------------------------
resource "aws_subnet" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.10${count.index}.0/24"
availability_zone = var.azs[count.index]
tags = {
Name = "private-subnet-${var.azs[count.index]}"
}
}
# ----------------------------------------------------------------------
# プライベート用ルートテーブルの作成(AZごとに独立させるのが肝!)
# ----------------------------------------------------------------------
resource "aws_route_table" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
# 自身のAZにあるNAT Gatewayへ向けてデフォルトルートを切る
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[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
}
このコードのポイントは、count を使ってAZのリスト分だけ、EIP、Public Subnet、NAT Gateway、Private Subnet、そして Private Route Table を完全に並列に生成している点です。これにより、AZ-a用のルートテーブルは必ずAZ-aのNAT Gatewayを向き、AZ-c用はAZ-cを向くという「AZ閉じ込め(AZローカリティ)」が美しく完結します。
—
4. アプリケーション層(Web API)からの接続確認と運用の注意点
マルチAZ NAT Gateway構成を導入した際、アプリケーション開発者が意識しなければならない仕様上の注意点があります。それが 「複数の送信元IPアドレス(EIP)からのアクセスを受け入れる準備」 です。
先ほどのTerraformコードではAZごとに異なるEIPをNAT Gatewayに割り当てました。これは、外部のサードパーティ製Web APIや決済代行サービスなどのアクセス元制限(IPホワイトリスティング)において非常に重要な意味を持ちます。
外部APIとの通信確認コード(Pythonの例)
プライベートサブネット内のコンテナから、自身の外向きIP(どのNAT Gatewayを通ったか)を確認するスニペットを以下に示します。
import requests
import json
def check_external_ip():
try:
# 自身のグローバルIPを返してくれるパブリックなエコサービスを利用
response = requests.get("https://httpbin.org/ip", timeout=5)
response.raise_for_status()
data = response.json()
print(f"[INFO] 現在の出口IPアドレス: {data.get('origin')}")
except requests.exceptions.RequestException as e:
print(f"[ERROR] 外部APIとの通信に失敗しました: {e}", file=sys.stderr)
if __name__ == "__main__":
check_external_ip()
このコードを実行すると、コンテナがデプロイされているAZに応じて、出力されるIPアドレスが切り替わります(例:AZ-a なら 203.0.113.10、AZ-c なら 203.0.113.20 など)。
現場でよくあるトラブルとTips
1. IPホワイトリストの漏れによる接続エラー
- 現象: マルチAZ化して数日後、特定のユーザーから「決済処理がランダムに失敗する」と報告が入る。
- 原因: 連携先の外部API側で、単一のNAT GatewayのIPしかホワイトリストに登録しておらず、別のAZのNAT Gatewayから飛んできたパケット(別のIP)がファイアウォールでブロックされていた。
- 対策: マルチAZ構成にする際は、すべてのNAT Gatewayに割り当てたEIPのリストを洗い出し、連携先企業のAPI管理者にすべて登録申請する必要があります。
2. クロスAZデータ転送コストの罠
- 現象: 予期せぬクラウドのネットワーク転送料金が高騰している。
- 原因: アプリケーションが
AZ-aで動いているのに、ルートテーブルの設定ミスでAZ-cのNAT Gatewayへトラフィックが跨いで(クロスAZ)ルーティングされていた。 - 対策: 必ずルートテーブルはAZごとに作成し、同一AZ内のNAT GatewayへルーティングされているかをAWS VPC Flow LogsやクラウドWatchのメトリクス(
CrossAZTrafficなど)で常時監視しましょう。
—
5. おわりに
ネットワークの設計は、平時においては「動いて当たり前」の黒衣のような存在です。しかし、ひとたび障害が発生したとき、そのシステムが真のレジリエンス(復元力)を持っているかどうかを分けるのは、こうした「マルチAZの原則に忠実な泥臭いインフラ設計」の積み重ねにほかならないと、数々のトラブルシューティングを潜り抜けて痛感しています。
「コストがかかるから」という理由でNAT Gatewayを1台にケチるのは、いわば「命綱を1本だけで高所作業をしている状態」です。ぜひ今回の記事を参考に、あなたのシステムのネットワーク基盤をマルチAZ化し、深夜のアラートにおびえない強固なシステムを構築してください。
コメント