VPCで一度決めたら実質やり直せないのはCIDR(IPアドレスの範囲)です。EC2もECSもRDSもこの上に乗るので、ここを詰めずに作り始めると、後から効いてきます。
この記事では、定番の3層×3AZ構成について、CIDRの採番ルール・IPが枯渇する条件の計算・Terraformでの書き方を整理します。コードは実際に terraform validate を通したものを載せています。
全体像

VPCを /16 で切り、3つのアベイラビリティゾーンに、パブリック・プライベート・データの3層のサブネットを置きます。よく見る形ですが、なぜ3層なのかを説明できるかどうかで設計の質が変わります。答えは後半に書きます。
なぜ /16 が定番なのか
VPCに指定できるCIDRは /16 から /28 までで、/16 が上限です(VPC CIDR blocks)。つまり「いちばん大きく取る」がそのまま /16 になります。大きく取る理由は3つです。
- あとから広げられない
既存のCIDRブロックはサイズを変更できない。足りなくなったら別のブロックを足す形になる - 大きく取っても料金は増えない
VPCとサブネットそのものは無料。お金がかかるのはNAT GatewayやVPCエンドポイントなどのリソース側 - 第3オクテットが丸ごと空く
10.0.x.0/24の形で256個の枠を使える。/20だと枠が16個しかなく、次の採番ルールが窮屈になる
逆に小さく切るのは、会社としてVPCを何十個も作る場合です。その場合は 10.0.0.0/8 をどう配るかという話になるので、次の次の節に書きます。
CIDRの採番|第3オクテットに意味を持たせる
VPCは 10.0.0.0/16 を基本にします。/24 のような小さいVPCで始めると、あとで広げられません。VPCのCIDRは作成後に変更できず、セカンダリCIDRを追加することはできても、ルーティングやセキュリティグループの設計が複雑になります。
サブネットの採番は、第3オクテットを見ただけで層が分かるように振ります。
| 層 | 採番 | ap-northeast-1a | 1c | 1d |
|---|---|---|---|---|
| パブリック | 1〜 | 10.0.1.0/24 | 10.0.2.0/24 | 10.0.3.0/24 |
| プライベート | 11〜 | 10.0.11.0/24 | 10.0.12.0/24 | 10.0.13.0/24 |
| データ | 21〜 | 10.0.21.0/24 | 10.0.22.0/24 | 10.0.23.0/24 |
この採番は Terraform の cidrsubnet() で機械的に出せます。実際に terraform console で計算した結果がこれです。
> [for i in range(3): cidrsubnet("10.0.0.0/16", 8, i + 11)]
[
"10.0.11.0/24",
"10.0.12.0/24",
"10.0.13.0/24",
]
手で書いた表と、コードが生成する値がずれるのはよくある事故です。ドキュメントの表は、コードから出した値を貼るようにすると食い違いません。
複数環境を作る場合は、第2オクテットで環境を分けます。将来 VPC Peering や Transit Gateway でつなぐときに、CIDRが重なっていると接続できないためです。
本番 10.0.0.0/16
検証 10.1.0.0/16
開発 10.2.0.0/16
すでに別のAWS環境がある場合の決め方
2つ目以降のVPCを作るときは、採番ルールより先に他の環境とレンジが重ならないことを確認します。重なっていると、あとでつなげなくなるからです。
- VPCピアリング
CIDRが重なっていると接続自体を作れない。複数のCIDRを持っている場合、そのうち1つでも重なっていれば作れない - Transit Gateway・Direct Connect
同じレンジが複数あると、どちらへ送るか決められないので通せない
やることは地味で、会社に台帳を1つ持つことです。10.0.0.0/8 を第2オクテットで割り当てると、見ただけで環境が分かります。第3オクテットから先は、この記事の採番ルールがそのまま使えます。
| 環境 | VPCのCIDR |
|---|---|
| 本番 | 10.0.0.0/16 |
| ステージング | 10.1.0.0/16 |
| 開発 | 10.2.0.0/16 |
| 共有(監視・CI・踏み台) | 10.9.0.0/16 |
避けるレンジも先に決めておきます。オンプレやVPN先で使っている範囲、オフィスや自宅で多い 192.168.0.0/16、それと 172.17.0.0/16 です。最後のひとつはDockerのデフォルトのブリッジネットワークや一部のAWSサービス(Cloud9、SageMaker)が使っていて、AWSのドキュメントにも「競合を避けるためVPCにこの範囲を使わないように」と書かれています。
VPCが数十個の規模になるなら、Amazon VPC IPAM でプールから払い出す方法もあります。ただしIPAMには管理するアクティブIPの数に応じた課金があり、東京リージョンではIP 1個あたり 1時間0.00027ドル(1,000個なら月およそ197ドル)です。数個のVPCなら、スプレッドシートの台帳で足ります。
IPは思っているより早く枯渇する
/24 なら256個のIPがあると思いがちですが、実際に使える数は違います。AWSは各サブネットで5つのIPを予約します。先頭のネットワークアドレス、VPCルーター用、DNS用、将来用に予約された1つ、末尾のブロードキャストアドレスです。
さらに効いてくるのがECS Fargateです。awsvpcモードのタスクは1つにつきENIを1つ持つので、タスク1個 = IP 1個を消費します。そしてローリング更新中は新旧のタスクが同時に立つため、瞬間的に2倍のIPが必要になります。
| サブネット | 総IP | AWS予約 | 使えるIP | 更新中も安全なタスク数 |
|---|---|---|---|---|
| /28 | 16 | 5 | 11 | 5 |
| /27 | 32 | 5 | 27 | 13 |
| /26 | 64 | 5 | 59 | 29 |
| /24 | 256 | 5 | 251 | 125 |
| /22 | 1,024 | 5 | 1,019 | 509 |
| /20 | 4,096 | 5 | 4,091 | 2,045 |
/24 のプライベートサブネットに置けるFargateタスクは、安全に見て125個です。ここにLambdaのVPC実行やVPCエンドポイントのENIも加わるので、実際の余裕はもっと少なくなります。
IP枯渇の怖いところは、普段は動いていて、デプロイした瞬間にだけ落ちることです。新タスクがENIを確保できずに起動失敗し、ロールバックもできない状態になります。プライベートサブネットだけ /22 にしておけば、この事故はほぼ起きません。

IPが足りなくなったらどうするか
サブネットもVPCも、一度作ったCIDRブロックのサイズは変えられません。足りなくなったときにできるのは、VPCにセカンダリCIDRを追加することです。1つのVPCに関連付けられるIPv4 CIDRブロックは既定で5個、申請すれば50個まで増やせます。
追加できるレンジには制限があります。プライマリが 10.0.0.0/8 の中にある場合、172.16.0.0/12 と 192.168.0.0/16 からは足せません。足せるのは同じ 10.0.0.0/8 の別ブロック、100.64.0.0/10、またはパブリックに割り当てられたIPv4レンジです。
実際の手順はこうなります。
- 消費を減らせないかを先に見る
インターフェース型のVPCエンドポイントはAZごとにENIを1つ使う。止め忘れたタスクや古いデプロイのENIが残っていることもある - セカンダリCIDRをVPCに追加する
ルートテーブルにはlocalのルートが自動で足される - 新しいサブネットを、今度は大きめに作る
プライベート層なら/22以上 - 使う側を移す
ECSはサービスのサブネット指定、LambdaはVPC設定、RDSはサブネットグループの変更。RDSは切り替えを伴うので作業時間を決めてから - 空になった古いサブネットを消す
ENIが1つでも残っていると削除できない
逃げ道としてよく使われるのが 100.64.0.0/10 です。RFC 1918 の外にある範囲で、社内やオンプレで使われていることが少ないので、Fargateのタスクのように外から直接アクセスしないものだけをそこへ寄せるという使い方ができます。
なぜ3層なのか|デフォルトルートで層が決まる
「パブリック」「プライベート」というサブネットの種類は、AWSの設定項目としては存在しません。ルートテーブルのデフォルトルート(0.0.0.0/0)がどこを向いているかだけが違いです。
| 層 | 0.0.0.0/0 の向き先 | できること |
|---|---|---|
| パブリック | Internet Gateway | 外から入れる/外へ出られる |
| プライベート | NAT Gateway | 外から入れない/外へは出られる |
| データ | 設定しない | 外から入れない/外へも出られない |
データ層の価値はここです。デフォルトルートを持たないサブネットに置いたRDSは、仮に認証情報が漏れても、そこからインターネットへデータを送り出す経路がありません。セキュリティグループの設定ミス1つで穴が開くプライベートサブネットとは、防御の層が違います。
「RDSはプライベートサブネットでいいのでは」と言われたときに説明できるのがこの違いです。2層で足りる案件も当然ありますが、3層にしないと決めるのと、3層を知らないのは別です。
NAT Gatewayをどこに置くか
NAT Gatewayは各AZのパブリックサブネットに1台ずつ置くのが基本です。1台に集約するとコストは下がりますが、そのNATがあるAZに障害が起きると、他のAZのプライベートサブネットまで外へ出られなくなります。マルチAZにした意味が消えるので、本番では避けたい構成です。
一方で開発・検証環境は1台集約で問題ありません。環境ごとにNATの台数を変えるのが現実的な落としどころです。
料金で見落としやすいのが、NAT Gatewayの課金が2本立てになっている点です。
- 時間課金
起動している限りかかる。台数に比例する - データ処理課金
通過したデータ量にかかる。こちらが見落とされやすい
単価はリージョンで違うので必ず公式の料金ページで確認してください。重要なのは、台数を減らすだけではデータ処理分は減らないということです。
効くのはVPCエンドポイントです。S3・ECR・CloudWatch LogsといったAWSサービス宛ての通信をNATから外すと、通過するデータ量そのものが落ちます。特にECRからのイメージ取得はサイズが大きいので、Fargateを使っているならゲートウェイ型のS3エンドポイントとECR用のインターフェース型エンドポイントは最初から入れておく価値があります。
ただしインターフェース型のエンドポイントはそれ自体が有料で、AZごとにENIを1つ消費します。IP消費の計算に入れ忘れないようにしてください。
環境ごとにアカウントを分ける|NATは共有できないのか
ここまではVPC1つの話でしたが、実務では開発・検証・本番でAWSアカウントを分けることが多いです。理由は次の4つです。
- 事故の範囲がアカウントで止まる
消し間違いも、付けすぎた権限も、本番には届かない - クォータがアカウント単位で決まる
VPCはリージョンあたり5個、ENIは5,000個といった上限がある。開発の使いすぎで本番が作れなくなるのを防げる - コストが自然に分かれる
タグを付け忘れても、アカウント単位では必ず分かれる - 本番だけ強く縛れる
Organizations のSCPで、本番アカウントだけ証跡の停止や想定外のリージョンの利用を禁止できる
ここでよく出るのが「アカウントを分けるとNAT Gatewayが環境の数だけ要るから高くなるのでは」という疑問です。ただ、NAT GatewayはVPCの中のリソースなので、同じアカウントに置いてもVPCが分かれていれば共有できません。ピアリングでつないでも、ピア先のNATは使えないとAWSのドキュメントに明記されています。台数を決めているのはアカウントの数ではなくVPCの数です。
それでも出口を1か所にまとめたいなら、方法は2つあります。
- VPCそのものを共有する
AWS RAM でサブネットを他のアカウントに共有する。アカウントは分けたまま、NATもVPCエンドポイントも1セットで済む(1つのVPCを共有できる参加アカウントは100まで) - 出口用のVPCを作って Transit Gateway で寄せる
TGWはアタッチメント1本あたり1時間0.07ドル(東京。月およそ51ドル)、通過するデータに1GBあたり0.02ドルが別でかかる
3環境を出口VPCに寄せるとアタッチメントは4本になり、それだけで月200ドルほど増えます。NATを各VPCに置く場合より安くなるかどうかは、通過するデータ量しだいです。先に効くのは、開発と検証のNATを1台に集約すること、VPCエンドポイントでNATを通る量を減らすことのほうです。
単価はAWSが公開している料金データ(東京リージョン、2026年9月17日版)から取りました。
Terraformで書く|countではなくfor_eachを使う
以下は Terraform v1.14.4 / AWS provider v6.63.0 で terraform validate と terraform fmt を通したものです。
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "azs" {
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}
locals {
# 第3オクテットで層を分ける(public=1〜 / private=11〜 / db=21〜)
public_cidrs = [for i, _ in var.azs : cidrsubnet(var.vpc_cidr, 8, i + 1)]
private_cidrs = [for i, _ in var.azs : cidrsubnet(var.vpc_cidr, 8, i + 11)]
db_cidrs = [for i, _ in var.azs : cidrsubnet(var.vpc_cidr, 8, i + 21)]
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "production" }
}
resource "aws_subnet" "private" {
for_each = { for i, az in var.azs : az => local.private_cidrs[i] }
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = each.value
tags = { Name = "private-${each.key}", Tier = "private" }
}
ポイントは count ではなく for_each を使っていることです。
count で書くと、リソースは aws_subnet.private[0] のようにインデックスで管理されます。あとからAZリストの途中を削ったり並べ替えたりすると、インデックスがずれて、関係のないサブネットまで削除して作り直す計画になります。サブネットの作り直しは、そこに乗っているリソースの作り直しを意味します。
for_each ならキーはAZ名なので、aws_subnet.private["ap-northeast-1c"] として管理されます。リストから別のAZを消しても、残りのサブネットは動きません。後から要素が増減する可能性があるものは for_each、というのが原則です。
やり直せるもの・やり直せないもの
| 項目 | あとから変更 | 備考 |
|---|---|---|
| VPCのCIDR | 不可 | セカンダリCIDRの追加は可能だが設計が複雑になる |
| サブネットのCIDR | 不可 | 作り直しになる |
| AZを増やす | 可 | 空いている番号にサブネットを追加するだけ |
| ルートテーブル | 可 | 層の性質を変えられる |
| セキュリティグループ | 可 | 運用中でも変更できる |
| NAT Gatewayの台数 | 可 | ルートテーブルの向き先を変える |
時間をかけて考えるべきなのは上の2行だけです。それ以外は運用しながら直せるので、最初から完璧を目指す必要はありません。設計レビューでもここに時間を割くべきです。
まとめ
- VPCは
/16
第2オクテットで環境、第3オクテットで層を表す - サブネットのIPは5つAWSに予約される
Fargateは1タスク1IP、更新中は2倍必要 - 層の違いはデフォルトルートの向き先
データ層は「向き先を設定しない」ことに意味がある - NATは本番でAZごと、開発は1台
データ処理課金はVPCエンドポイントで減らす - Terraformは
for_eachcountはAZ構成が変わったときに壊れる - 足りなくなったらセカンダリCIDRを足して新しいサブネットへ寄せる
既存のサブネットは広げられない - 環境ごとにアカウントを分ける
NATはVPC単位なので、分けても台数は増えない - 後から変えられないのはCIDRだけ
そこ以外は運用で直せる
構成図の描き方についてはAWS構成図の書き方にまとめています。この記事の図も、そこで書いたルールで描いています。
次に読む記事
