目次
28 章

VPC 深掘り — Subnet 設計 · Peering · Transit Gateway · PrivateLink

第8章で押さえた VPC の基礎を production 規模へ引き上げます。3-tier / 4-tier の Subnet 設計と CIDR 計画、NAT · Egress-only IGW · VPC Endpoint でインターネットの出入りを Terraform コードとコスト計算まで扱い、VPC Peering と Transit Gateway で VPC をつなぐ方法、PrivateLink、IPv6 dual-stack、マルチ VPC のメンタルモデルまでを整理します。

5部 運用 · セキュリティ · コストの最初の章です。4部まで、私たちは単一 VPC の中で ECS Fargate 上に1つのサービスを載せて運用してきました。第8章 EC2 と VPC の基礎 で VPC · Subnet · ルーティングテーブル · インターネットゲートウェイ · セキュリティグループの基本を押さえたなら、この章はその上で production 規模のネットワークをどう設計するか を扱います。

運用に入ると単一の public Subnet 1つでは足りません。DB はインターネットから届いてはならず、複数の環境と複数の VPC が生まれ、プライベートな区間からも AWS サービスに届く必要があります。この章で押さえる Subnet 階層 · VPC 間の接続 · プライベートな出入りモデルは、第29章 セキュリティガバナンス のマルチアカウントや、6部 フルスタックアプリを AWS にデプロイする のインフラ骨格へそのままつながります。この章のコードは 第25章 Terraform 入門 の HCL を前提とします。

第8章から5部へ #

第8章 で扱ったのは次のとおりです。

  • VPC はアカウント内に作るプライベートネットワークであり、CIDR ブロック(例: 10.0.0.0/16)で IP 範囲を定めます。
  • Subnet は VPC の中を AZ 単位で区切った区画です。
  • インターネットゲートウェイ(IGW)にルーティングがつながった Subnet が public、そうでない Subnet が private です。
  • セキュリティグループはインスタンス単位のファイアウォール、ネットワーク ACL は Subnet 単位のファイアウォールです。

この章は、その基本要素を 複数階層 · 複数 AZ · 複数 VPC へ拡張します。

Subnet 設計 — 3-tier と 4-tier #

production VPC の基本骨格は 3-tier です。役割の異なる Subnet を階層に分けます。

階層インターネット入るものデフォルトルーティング
publicインバウンド / アウトバウンドALB、NAT Gateway、Bastion0.0.0.0/0 → IGW
private (app)アウトバウンドのみECS / EC2 アプリ、Lambda(VPC)0.0.0.0/0 → NAT Gateway
isolated (data)なしRDS、ElastiCacheローカル + VPC Endpoint のみ

規制の強い環境では、ここに 管理専用階層(Bastion / 運用ツール)を加えて 4-tier に分けることもあります。要点は DB が住む isolated 階層はインターネットへ出る経路そのものがない ということです。アプリは private から NAT を通って出ていき、DB はどこへも出ていきません。階層の間のアクセスはセキュリティグループで一方向ずつだけ開けます — ALB → app(8000)、app → DB(5432) のように 送信元のセキュリティグループを参照 し、IP ではなく役割で制御します。

階層間のアクセスをセキュリティグループ参照で制御
resource "aws_security_group_rule" "app_from_alb" {
  type                     = "ingress"
  security_group_id        = aws_security_group.app.id
  source_security_group_id = aws_security_group.alb.id   # ALB から来たものだけ
  from_port                = 8000
  to_port                  = 8000
  protocol                 = "tcp"
}

resource "aws_security_group_rule" "db_from_app" {
  type                     = "ingress"
  security_group_id        = aws_security_group.db.id
  source_security_group_id = aws_security_group.app.id   # app から来たものだけ
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
}

CIDR 計画 #

CIDR は一度決めると変えるのが面倒です。最初に余裕をもって、規則的に切ります。2 AZ × 3階層の例は次のとおりです。

VPC 10.0.0.0/16 の Subnet 分割 (2 AZ × 3 tier)
VPC            10.0.0.0/16     (65,536 IP)

AZ a
  public       10.0.0.0/20     (4,096)
  private-app  10.0.16.0/20    (4,096)
  isolated-db  10.0.32.0/20    (4,096)
AZ c
  public       10.0.128.0/20   (4,096)
  private-app  10.0.144.0/20   (4,096)
  isolated-db  10.0.160.0/20   (4,096)
  • VPC は /16 で大きく取ります。あとから階層や AZ を追加する余地を残します。
  • 各 Subnet は /20(4,096 IP)程度あれば ECS Task が増えても IP が枯渇しません。Fargate Task 1つが ENI 1つ = IP 1つ を使うので、コンテナ密度が高いと IP 枯渇が実際の障害につながります。AWS が予約する Subnet あたり5個(ネットワーク/ブロードキャスト/ゲートウェイなど)も考慮します。
  • AZ 同士で上位ビットを離して(10.0.0.x vs 10.0.128.x)、あとから読みやすくします。
  • 他の VPC と Peering する可能性があるなら CIDR が重ならないように します。重なった2つの VPC は Peering できません。組織レベルでは VPC ごとに 10.0.0.0/1610.1.0.0/16 のようにあらかじめ番号を割り当てておきます。

ルーティングテーブル #

階層ごとにルーティングテーブルを別々に置きます。ルーティングの違いがすなわち階層の定義です。

階層別ルーティングテーブル
# public: インターネットへ双方向
resource "aws_route" "public_internet" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.main.id
}

# private-app: AZ 別の NAT でアウトバウンドのみ
resource "aws_route" "private_nat" {
  for_each               = aws_nat_gateway.az          # AZ ごとに1つ
  route_table_id         = aws_route_table.private[each.key].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = each.value.id
}

# isolated-db: デフォルト経路なし — local のみ。インターネット遮断
# (aws_route_table.isolated  0.0.0.0/0 経路を置かない)

isolated-db のルーティングテーブルに 0.0.0.0/0 経路を 置かないこと が、すなわち DB をインターネットから切り離す方法です。

インターネット出入り — IGW · NAT · Egress-only IGW #

private 階層のアプリは外へ出る必要が多くあります(パッケージのインストール、外部 API 呼び出し、ECR pull)。ところがインバウンドは塞ぐ必要があります。この非対称を解くのが NAT です。

装置方向対象備考
Internet Gateway (IGW)双方向public SubnetVPC あたり1個、無料
NAT Gatewayアウトバウンドのみ (IPv4)private Subnet時間あたり + 処理 GB あたり課金。AZ ごとに1個推奨
Egress-only IGWアウトバウンドのみ (IPv6)private Subnet無料。IPv6 専用
AZ ごとに NAT Gateway 1つ
resource "aws_eip" "nat" {
  for_each = toset(["a", "c"])
  domain   = "vpc"
}

resource "aws_nat_gateway" "az" {
  for_each      = aws_eip.nat
  allocation_id = each.value.id
  subnet_id     = aws_subnet.public[each.key].id   # NAT は public に住む
}

NAT Gateway のコスト注意 #

NAT Gateway は運用の請求書で意外に大きな項目です。時間あたり料金(ソウル基準で約 $0.059/h、NAT 1つあたり月に約 $43)に加えて 処理したデータ GB あたり(約 $0.059/GB)課金されます。そのため private アプリが S3 / ECR / DynamoDB のようにトラフィックの多い AWS サービスを NAT 経由で出入りすると、コストが速く積み上がります。

たとえば AZ 2個に NAT を置き、月に 500GB を処理すると、おおよそ次のようになります。

NAT コスト概算 (ソウル, 2 AZ, 月 500GB)
時間あたり $0.059 × 24h × 30日 × 2個  ≈ $85
処理量     $0.059 × 500GB             ≈ $30
                                     ─────────
                                      約 $115 / 月

減らす方法は次のとおりです。

  • AZ ごとに NAT Gateway を1つ — AZ 間のデータ転送コストを避けます。単一 NAT にまとめると安く見えますが、AZ 障害時に全 AZ のアウトバウンドが断たれ、他の AZ のアプリが NAT のある AZ へ越境して AZ 間転送料がつきます。
  • VPC Endpoint で NAT を迂回 — S3 と DynamoDB は Gateway Endpoint が無料です。ECR · CloudWatch · Secrets Manager のトラフィックも Interface Endpoint へ送れば NAT の処理コストから外れます。コンテナ環境では ECR pull はトラフィックが大きいので効果が大きいです。
  • 学習 / dev 環境は NAT の代わりにアプリを public Subnet に置く、または必要なときだけ NAT を起動するのも方法です。

このコストの観点は、第27章 コスト最適化第30章 災害復旧・バックアップ のマルチ AZ 設計と併せて見ます。

VPC Endpoint と PrivateLink #

private / isolated 階層からも AWS サービス(S3、Secrets Manager、ECR など)には届く必要があります。このときインターネットへ出てから戻る代わりに、VPC Endpoint でプライベートな経路を作ります。

種類対象動作コスト
Gateway EndpointS3、DynamoDBルーティングテーブルに経路を追加無料
Interface Endpoint (PrivateLink)ほとんどの AWS サービス、自作のサービスSubnet に ENI を生成、プライベート DNS時間あたり + GB あたり
Gateway Endpoint(S3, 無料) + Interface Endpoint(ECR)
# S3 — ルーティングテーブルに付く無料の Gateway Endpoint
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-northeast-2.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [for rt in aws_route_table.private : rt.id]
}

# ECR — private subnet に ENI ができる Interface Endpoint
resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-northeast-2.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [for s in aws_subnet.private : s.id]
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true   # 既存の ECR ドメインがプライベート IP に解決される
}

private_dns_enabled = true なら *.ecr.ap-northeast-2.amazonaws.com のような既存のドメインが自動で Endpoint のプライベート IP に解決され、アプリのコードを変える必要がありません。

PrivateLink は、ある VPC のサービスを別の VPC やアカウントに インターネットなしで 公開する技術でもあります。あるチームが作った内部 API の前に NLB を置いて aws_vpc_endpoint_service で公開すると、消費者側の VPC は Interface Endpoint でそのサービスに接続します。2つの VPC の CIDR が重なってもプライベートに接続できるのが、Peering に対する PrivateLink の強みです。第20章 Secrets Manager / Parameter Store のシークレットを isolated アプリが読むときも、Interface Endpoint がすっきりした経路です。

VPC 同士をつなぐ — Peering と Transit Gateway #

VPC が2つ以上になると(環境分離、アカウント分離、買収したシステムなど)、プライベートにつなぐ2つの方法があります。

VPC Peering #

2つの VPC を 1:1 で直接接続します。接続を作ったあと、両側のルーティングテーブルに相手の CIDR 経路を直接入れて初めて 通じます。

VPC Peering + 双方向の経路
resource "aws_vpc_peering_connection" "a_to_b" {
  vpc_id      = aws_vpc.a.id
  peer_vpc_id = aws_vpc.b.id
  auto_accept = true            # 同じアカウント/リージョンのとき
}

resource "aws_route" "a_to_b" {
  route_table_id            = aws_route_table.a.id
  destination_cidr_block    = aws_vpc.b.cidr_block   # 10.1.0.0/16
  vpc_peering_connection_id = aws_vpc_peering_connection.a_to_b.id
}

resource "aws_route" "b_to_a" {
  route_table_id            = aws_route_table.b.id
  destination_cidr_block    = aws_vpc.a.cidr_block   # 10.0.0.0/16
  vpc_peering_connection_id = aws_vpc_peering_connection.a_to_b.id
}
  • 設定が単純で、同じ / 異なるアカウント · リージョンのいずれも可能です。
  • 非推移的(non-transitive) です。A–B、B–C を Peering しても A–C は自動では通りません。VPC が N 個あれば接続が N(N-1)/2 個に増え、5個になるだけで10個の Peering とその2倍の経路を管理することになります。
  • 両側の CIDR が重なってはいけません。

Transit Gateway #

複数の VPC を1つのハブに付ける方式です。

Transit Gateway ハブ + VPC アタッチ
resource "aws_ec2_transit_gateway" "hub" {
  description                     = "org hub"
  default_route_table_association = "enable"
  default_route_table_propagation = "enable"
}

resource "aws_ec2_transit_gateway_vpc_attachment" "a" {
  transit_gateway_id = aws_ec2_transit_gateway.hub.id
  vpc_id             = aws_vpc.a.id
  subnet_ids         = [for s in aws_subnet.a_private : s.id]
}
# VPC b, c... も同じように1回ずつだけ付ける
  • 各 VPC を Transit Gateway に1回ずつ付ければよいです(スター構造)。VPC が増えても接続数が線形にしか増えません。
  • TGW ルーティングテーブル でどの VPC 同士が通信するかを制御します。たとえば dev と prod を互いに見えないように分離しつつ、両方とも共有サービス VPC には届くようにできます。オンプレミス(VPN / Direct Connect)も同じハブに付きます。
  • 時間あたり(アタッチあたり)+ 処理 GB あたり課金があるため、VPC が 2 ~ 3個と少ないときは Peering の方が安いです。

判断基準: VPC が 2 ~ 3個なら Peering、それ以上に増える兆しが見えれば Transit Gateway です。マルチアカウントガバナンス(第29章)へ向かう組織は、最初から Transit Gateway を敷いておく方が後で楽です。6部キャップストーンは単一 VPC なのでどちらも使いませんが、2つ目の環境用アカウントが生まれた瞬間にこの判断が登場します。

IPv6 導入の判断ガイド #

IPv6 はすべてのプロジェクトに必要なわけではありません。次のときに検討します。

  • アウトバウンドトラフィックが多く NAT Gateway のコスト が負担なとき。IPv6 のアウトバウンドは無料の Egress-only IGW で処理されるので、NAT コストを減らせます。
  • パブリック IPv4 アドレスが 有料(2024年2月から、使用中のパブリック IPv4 1つあたり時間あたり課金)なので、大量のパブリック IP が必要なワークロードでコスト削減の余地があるとき。
  • 外部から IPv6 で入ってくるクライアントを直接受ける必要があるとき。

ほとんどは dual-stack(IPv4 + IPv6 同時)で導入します。

dual-stack — VPC/Subnet に IPv6 追加 + Egress-only IGW
resource "aws_vpc" "main" {
  cidr_block                       = "10.0.0.0/16"
  assign_generated_ipv6_cidr_block = true     # AWS が /56 IPv6 を割り当て
}

resource "aws_egress_only_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id                     # IPv6 アウトバウンド (無料)
}

resource "aws_route" "private_ipv6" {
  route_table_id              = aws_route_table.private["a"].id
  destination_ipv6_cidr_block = "::/0"
  egress_only_gateway_id      = aws_egress_only_internet_gateway.main.id
}

単純なバックエンド1台なら IPv4 だけで十分なので、IPv6 はコストや要件が明確なときに入れます。

マルチ VPC のメンタルモデル #

VPC をいつ分けるかの基準です。

  • 単一 VPC + 階層 Subnet — 1つのサービス、1つのチーム、1つの環境ならこの骨格で十分です。4部まで私たちが使ってきたモデルです。
  • 環境別 VPC — dev / staging / prod を VPC で分けると、事故の爆発半径が環境の中に閉じ込められます。ただし本当の隔離は アカウント分離(第29章)の方が強いです。
  • アカウント + VPC の組み合わせ — 規模が大きくなれば、アカウントを環境 / チーム別に分け、各アカウントに VPC を置き、Transit Gateway で必要な分だけつなぎます。これが production 組織の一般的な姿です。

VPC を分ける一次的な動機は、たいてい セキュリティ境界と爆発半径 です。その深い話は、次の 第29章 セキュリティガバナンス の Organizations · SCP へつながります。

練習問題 #

  1. ご自身のサービスの VPC を 10.0.0.0/16 とし、2 AZ × 3-tier(public / private-app / isolated-db)で Subnet 6個の CIDR を自分で割り当ててみてください。各 Subnet のルーティングテーブルが IGW / NAT / ローカルのうち何をデフォルト経路に持つかを1行ずつ書き、isolated 階層になぜ 0.0.0.0/0 経路を置かないかを説明してみてください。第32章 フルスタックアプリを AWS にデプロイする の Terraform VPC モジュールを書くとき、この表がそのまま入力になります。
  2. private アプリが ① S3、② ECR(イメージ pull)、③ 外部決済 API の3か所へアウトバウンドするとき、それぞれを NAT Gateway · Gateway Endpoint · Interface Endpoint のどれで送ればコストが最小になるかと、その理由を §「NAT Gateway のコスト注意」の概算を根拠に書いてみてください。
  3. VPC が現在2個で、1年以内に 5 ~ 6個に増える計画があるなら、Peering と Transit Gateway のどちらで始めるかを決め、接続数(N(N-1)/2 vs 線形)とルーティング管理の負担を根拠に1段落で書いてみてください。

一行まとめ: production VPC は public / private-app / isolated-db の 3-tier で設計し、DB 階層はルーティングテーブルに 0.0.0.0/0 経路そのものを置かないことでインターネットから切り離す。階層間のアクセスはセキュリティグループ参照で役割ベースに開ける。private アプリのアウトバウンドは AZ 別の NAT Gateway へ送りつつ、S3 · DynamoDB は無料の Gateway Endpoint、ECR などは Interface Endpoint で迂回して時間あたり + GB あたりの NAT コストを減らす。VPC が 2 ~ 3個なら Peering(非推移的、両側の経路を手動)、さらに増えたら Transit Gateway(スターハブ、TGW ルーティングで制御)でつなぐ。PrivateLink は CIDR が重なってもプライベートにサービスを公開し、IPv6 は NAT/パブリック IPv4 のコストが負担なときに dual-stack + Egress-only IGW で導入する。VPC を分ける一次的な動機はセキュリティ境界と爆発半径。

次の章 #

次の 第29章 セキュリティガバナンス では、単一アカウントからマルチアカウントへ移る時点を扱います。AWS Organizations · SCP · Control Tower でアカウントをまとめてポリシーを一括適用するモデル、そして GuardDuty · Security Hub · Config でアカウント全体を監視するガバナンスを整理します。この章のマルチ VPC がマルチアカウントへ拡張される自然な次のステップです。

X