なぜ Web サーバーは「パブリックサブネット」に置き、DB は「プライベートサブネット」に置くのか?
Terraform の module や構成図で「public-subnet」「private-subnet」という名前を何度も見てきたはずだ。だがその名前は AWS が勝手に決めているわけではない。実体はただの IP アドレスの範囲であり、「パブリック」か「プライベート」かは、たった 1 本のルーティング設定で決まる。その 1 本が何なのかを説明できるだろうか。
- サブネットに「パブリック」「プライベート」という種類は元々備わっておらず、そのサブネットに紐づくルートテーブルに
0.0.0.0/0 → インターネットゲートウェイの経路があるかどうかだけで区別される。 - プライベートサブネットのインスタンスを外部からの直接到達不可のまま外向き通信だけ許可するのが NAT ゲートウェイで、送信元 IP をインスタンスの private IP から自分の Elastic IP に付け替えて中継する。
- サブネットは 1 つのアベイラビリティーゾーン(AZ)をまたげないため、AZ 障害への耐性は「複数 AZ にサブネットを分けて配置する」という運用側の設計でしか作れない。
VPC は「区切られた住所空間」、サブネットはその中の街区
VPC(Virtual Private Cloud)は、自分の AWS アカウント専用に論理的に切り離された仮想ネットワークで、作成時に 10.0.0.0/16 のような IP アドレスの範囲(CIDR ブロック)を1つ指定する。この時点ではまだ、どの IP をどこで使うかは何も決まっていない。
サブネットは、その VPC の CIDR をさらに小さく切り出した IP アドレスの範囲にすぎない。EC2 インスタンスや RDS はこのサブネットのどれかに所属する形で起動する。サブネットはどれか 1 つの AZ に完全に収まり、複数の AZ にまたがることができない。この制約があるからこそ、「AZ 障害に耐えるには、同じ役割のサブネットを複数の AZ に用意して分散させる」という設計パターンが生まれる。1 つの AZ にしかサブネットを作らなければ、その AZ が丸ごと落ちた瞬間にそのサブネット上の全リソースが道連れになる。
「パブリック」を決めているのはルートテーブルであって、サブネット自身ではない
サブネットには、送信先ごとにどの経路へ流すかを定義したルートテーブルが必ず1つ紐づいている(明示的に関連付けなければ、VPC の「メインルートテーブル」に自動的に従う)。あるサブネットが「パブリック」と呼ばれるのは、そのルートテーブルに 送信先 0.0.0.0/0 → ターゲット インターネットゲートウェイ(igw) という経路が入っているからにすぎない。この経路が無く、代わりに NAT ゲートウェイ経由の経路しか無ければ「プライベートサブネット」になる。
つまり同じサブネットでも、ルートテーブルの中身を書き換えれば呼び方は変わる。パブリック/プライベートは AWS が管理する固定属性ではなく、ルーティングの結果として観測される状態だ。この理解が無いと、「public_subnet_ids という変数に入れたから公開されるはず」という思い込みで、実際にはルートが刺さっておらず外に出られない、という Terraform あるあるにハマる。
VPC 10.0.0.0/16
┌─────────────────────────────┐
│ AZ-a │ AZ-c │
│┌───────────┐ │┌───────────┐ │
││public │ ││public │ │
││10.0.1.0/24│ ││10.0.2.0/24│ │
││ → igw │ ││ → igw │ │
│└─────┬─────┘ │└─────┬─────┘ │
│ ▼NAT-GW │ ▼NAT-GW │
│┌───────────┐ │┌───────────┐ │
││private │ ││private │ │
││10.0.11.0 │ ││10.0.12.0 │ │
││ → NAT-GW │ ││ → NAT-GW │ │
│└───────────┘ │└───────────┘ │
└───────┬───────────────┬─────┘
▼ ▼
internet gateway(igw)
▼
internet
マンションの部屋番号そのものは「表向きの部屋」でも「奥の部屋」でもない。表通りに面した出入り口に直結しているか、それとも管理人室を経由しないと外に出られない構造になっているか、という配管の違いだけが「表か奥か」を決めている。部屋(サブネット)の番地はどちらも同じ体系の中の一区画にすぎず、外への配管(ルートテーブル)をどうつなぐかで役割が変わる。
NAT ゲートウェイ — 「出られるが、入れない」を作る仕組み
DB やバッチサーバーのように、外部から直接アクセスされたくないが、パッケージの取得や外部 API 呼び出しなど自分からの通信は必要、というインスタンスをプライベートサブネットに置くと、そのままではインターネットに出られない。NAT ゲートウェイはこの「自分からの通信だけ許可し、外から新規に話しかけられることは拒否する」という非対称性を実現する。
仕組みは、プライベートサブネットのインスタンスが送ったパケットの送信元 IP を、いったん NAT ゲートウェイ自身の private IP に付け替え、さらにパブリックサブネット経由でインターネットゲートウェイへ渡す際に NAT ゲートウェイに紐づく Elastic IP へ付け替える、という 2 段のアドレス変換だ。応答パケットが返ってくると逆の変換をたどって元のインスタンスに届く。この変換テーブルは NAT ゲートウェイ側が「自分から出た通信の応答だ」と認識できる間しか維持されないため、外部から新規にコネクションを張ろうとしても対応する変換エントリが無く弾かれる。
NAT ゲートウェイ自身はパブリックサブネットに置き、Elastic IP を1つ持たせて作成する。1 つの NAT ゲートウェイを全 AZ のプライベートサブネットで共有する構成も動くには動くが、その AZ が落ちれば他の AZ のプライベートサブネットまで道連れで外向き通信を失う。AZ ごとにルートテーブルを分け、それぞれの AZ に置いた NAT ゲートウェイへ向けるのが、単一障害点を作らないための定石だ。
# private サブネットのルートテーブル(AZ-a 用) Destination Target 10.0.0.0/16 local 0.0.0.0/0 nat-xxxxxxxx (同じ AZ-a の NAT Gateway) # → igw への経路は無い。NAT-GW を経由しないと外に出られない
デフォルト VPC はなぜ「最初から動く」ように見えるのか
AWS アカウントには各リージョンにデフォルト VPC があり、各 AZ にデフォルトサブネットが1つずつ、インターネットゲートウェイもすでにアタッチ済みで、メインルートテーブルには最初から 0.0.0.0/0 → igw の経路が入っている。つまりデフォルトサブネットはすべて最初からパブリックだ。何も考えずに EC2 を起動してもすぐ SSH できるのはこのためで、裏を返せば、学習用の検証環境ならそのままでよいが、本番の DB やバックエンドをうっかりデフォルト VPC のデフォルトサブネットに置くと、意図せず全世界に到達可能な場所に置いてしまうことになる。自分で作った VPC(nondefault VPC)ではこの経路は自動では入らないため、パブリックサブネットが必要なら明示的にルートを追加する必要がある。
Terraform の VPC module(例えば terraform-aws-modules/vpc/aws)を使うと、AZ 数 × (public + private) 分のサブネットとルートテーブル、NAT ゲートウェイが一括で作られる。ここで single_nat_gateway = true のような設定を見たら、それはコスト最適化のために単一障害点を意図的に受け入れている選択だと読み取れる必要がある。ALB は public サブネットの ID を、EC2 や RDS は private サブネットの ID を渡す変数がそれぞれ分かれているのも、上で見たルートの違いをそのまま反映したものだ。
NAT ゲートウェイは自前で作ると課金が発生するため、ここでは既存の AWS アカウントを持っている場合に、手元の VPC の各サブネットがパブリックかプライベートかをルートテーブルの中身から自分で判定してみる。
# VPC 内の全ルートテーブルと、そのターゲットを一覧表示する
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=vpc-xxxxxxxx" \
--query "RouteTables[].{RT:RouteTableId,Subnets:Associations[].SubnetId,Targets:Routes[?DestinationCidrBlock=='0.0.0.0/0'].{GW:GatewayId,NAT:NatGatewayId}}" \
--output table
GW の列に igw- で始まる ID が出ていればそのルートテーブルに紐づくサブネットはパブリック、NAT の列に nat- で始まる ID しか出ていなければプライベートだと判定できる。同じ VPC の中でも、ルートテーブルごとにこの結果がきれいに分かれているはずだ。
- パブリックサブネットかどうかはサブネット自体に設定された属性だ — 実際にはサブネットに紐づくルートテーブルの経路だけで決まる。同じサブネットでもルートテーブルの内容を変えれば区分は変わる。
- NAT ゲートウェイを1つ作れば、どの AZ のプライベートサブネットからでも同じように使い回せる — 動作はするが、その NAT ゲートウェイがあるAZが落ちると他 AZ のプライベートサブネットまで外向き通信を失う。AZ ごとに NAT ゲートウェイを分けて同一 AZ 内で完結させるのが単一障害点を避ける構成だ。
- プライベートサブネットに置けば、そのインスタンスはインターネットと一切通信できない — インバウンドの新規接続を受けられないだけで、NAT ゲートウェイ経由でアウトバウンド通信(パッケージ取得や外部 API 呼び出しなど)は行える。
- VPC
- AWS アカウント専用に論理的に切り離された仮想ネットワーク。CIDR ブロックで IP アドレス範囲を持つ。
- サブネット
- VPC の CIDR を切り出した IP アドレス範囲。必ず1つの AZ に収まり複数 AZ をまたげない。
- ルートテーブル
- 送信先ごとにどのターゲットへトラフィックを流すかを定義する経路の集合。サブネットに1つ紐づく。
- インターネットゲートウェイ(igw)
- VPC をインターネットに接続するコンポーネント。ルートに存在するかどうかがパブリック判定の基準。
- NAT ゲートウェイ
- プライベートサブネットのアウトバウンド通信のみを中継し、インバウンドの新規接続は通さない仕組み。
- How Amazon VPC works | AWS 公式ドキュメント — VPC・サブネット・ルートテーブル・インターネットゲートウェイの関係を定義する一次情報。
- Subnets for your VPC | AWS 公式ドキュメント — サブネットが1つの AZ に収まる制約と、パブリック/プライベート/isolated の分類基準。
- NAT gateways | AWS 公式ドキュメント — NAT ゲートウェイのアドレス変換の仕組みとpublic/privateの接続タイプの違い。