02. VPC とネットワーク
AWS のほぼ全リソースは VPC(仮想ネットワーク) の上で動きます。最初の VPC を作り、パブリック/プライベートのサブネットに分け、インターネットと出入りできるようにルーティングする。これが AWS インフラの「土台 50%」です。
この章の目次
登場人物の早見表
| 用語 | 役割 | たとえると |
|---|---|---|
| VPC | AWS 上のあなた専用の仮想ネットワーク | マンション全体の専有エリア |
| Subnet | VPC を切り分けた区画。1 つの AZ に紐づく | マンションの各フロア |
| Internet Gateway (IGW) | VPC をインターネットにつなぐ玄関 | 建物のメイン出入口 |
| Route Table | 「このサブネットの通信は、ここに送る」のルール表 | 各部屋の郵便配送ルール |
| NAT Gateway | プライベートサブネットの「外向きだけ」通信を可能にする | 外には出られるが、外からは入れない通用口 |
| Elastic IP (EIP) | 固定のグローバル IP アドレス | NAT Gateway 等に貼り付ける番地 |
| Security Group (SG) | リソース単位のファイアウォール | 各部屋のドアの鍵 |
標準トポロジ図
2 つの AZ にまたがる、Web 3 層構成の標準図です。
ポイントは、サブネットの種類はネットワーク的に決まる こと。「パブリックサブネット」とは、ルートテーブルが 0.0.0.0/0 → IGW を持っているサブネットのことを指します。AWS のサブネット属性ではなく ルーティング で決まる。
aws_vpc
# VPC = AWS 上の専有ネットワーク領域。最初に作るのが基本
# cidr_block で IP アドレスの範囲を決める(/16 が最大、/28 が最小)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16" # /16 で 65,536 IP 確保
enable_dns_support = true # VPC 内 DNS 解決を有効化
enable_dns_hostnames = true # EC2 にパブリック DNS が付与される
tags = {
Name = "main"
}
}
覚えておきたい属性
| 属性 | 用途 |
|---|---|
id | 他リソースから VPC を参照する |
arn | IAM ポリシー条件に書く |
default_security_group_id | デフォルト SG。使わない のが原則 |
main_route_table_id | VPC のメインルートテーブル |
aws_subnet
# 現在のリージョンの利用可能 AZ 一覧を取得(ハードコード回避)
data "aws_availability_zones" "available" {
state = "available"
}
# パブリックサブネット × 2(各 AZ に 1 つずつ)
# for_each で「キー = AZ サフィックス、値 = CIDR と AZ 名の組」の map を回す
resource "aws_subnet" "public" {
for_each = {
"a" = { cidr = "10.0.1.0/24", az = data.aws_availability_zones.available.names[0] }
"c" = { cidr = "10.0.2.0/24", az = data.aws_availability_zones.available.names[1] }
}
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr # 各 subnet の CIDR
availability_zone = each.value.az # 各 subnet の AZ
map_public_ip_on_launch = true # ここに置く EC2 は public IP 自動付与
tags = {
Name = "public-${each.key}" # public-a, public-c
Tier = "public"
}
}
# プライベートサブネット × 2(構造は public と同じ、map_public_ip_on_launch なし)
resource "aws_subnet" "private" {
for_each = {
"a" = { cidr = "10.0.11.0/24", az = data.aws_availability_zones.available.names[0] }
"c" = { cidr = "10.0.12.0/24", az = data.aws_availability_zones.available.names[1] }
}
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
Name = "private-${each.key}"
Tier = "private"
}
}
aws_internet_gateway / aws_route_table
# Internet Gateway = VPC を外部インターネットに接続する玄関
# VPC ごとに 1 つだけアタッチできる
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "main" }
}
# ルートテーブル = subnet ごとの「どこに向かう通信をどこに送るか」のルール表
# 下の route ブロック: 0.0.0.0/0(インターネット全宛先)を IGW に送る
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "public" }
}
# Subnet に Route Table を関連付け(これでパブリック化が完成)
# for_each で aws_subnet.public の全要素を回す
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
aws_nat_gateway / aws_eip
プライベートサブネットの中の EC2 が 外向き通信(パッケージ更新、外部 API 呼び出し)をしたい時に必要。NAT Gateway は パブリックサブネットに置く。
# Elastic IP = NAT Gateway に貼り付ける固定のグローバル IP
# domain = "vpc" で VPC 用に確保(EC2-Classic 用ではない)
resource "aws_eip" "nat" {
domain = "vpc"
tags = { Name = "nat" }
}
# NAT Gateway = プライベート subnet から外向きインターネット通信するための窓口
# Public subnet に配置する必要がある(IGW へのルートがあるため)
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public["a"].id # public 側に置く
tags = { Name = "main" }
depends_on = [aws_internet_gateway.main] # IGW があってからじゃないと NAT は動かない
}
# プライベート用のルートテーブル: 0.0.0.0/0 を NAT Gateway に向ける
# → プライベート subnet の EC2 が外部 API を叩けるようになる
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = { Name = "private" }
}
# プライベート subnet をプライベート Route Table に紐付け
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
コスト注意
NAT Gateway は 1 時間あたり $0.045 + データ処理量。1 か月で $35 + α。学習や開発環境で常時上げっぱなしにするとボディブローのように効きます。
terraform destroy を忘れずに。
aws_security_group の現代的書き方
以前は ingress { ... }、egress { ... } を inline で書く形が主流でしたが、2023 年以降は分離型のリソース が公式推奨です。差分が小さくなり、ルール単位で管理できます。
# Security Group 本体(ルールは別 resource に分離するモダンスタイル)
# name_prefix を使うと、Terraform がサフィックスを自動付与(重複回避)
resource "aws_security_group" "web" {
name_prefix = "web-"
description = "Web tier (ALB)"
vpc_id = aws_vpc.main.id
lifecycle {
create_before_destroy = true # 入れ替え時に通信断を起こさない
}
tags = { Name = "web" }
}
# 個別 ingress ルールは別リソースで(差分管理が楽、推奨パターン)
# HTTP (80) をどこからでも許可
resource "aws_vpc_security_group_ingress_rule" "web_http" {
security_group_id = aws_security_group.web.id
description = "HTTP from anywhere"
from_port = 80
to_port = 80
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
}
# HTTPS (443) 用ルール
resource "aws_vpc_security_group_ingress_rule" "web_https" {
security_group_id = aws_security_group.web.id
from_port = 443
to_port = 443
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
}
# 外向き(egress)はすべて許可
# ip_protocol = "-1" は「すべてのプロトコル」の意味
resource "aws_vpc_security_group_egress_rule" "web_all" {
security_group_id = aws_security_group.web.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
# アプリ層用 SG(Web SG からの通信だけ受ける、内部ネットワーク)
resource "aws_security_group" "app" {
name_prefix = "app-"
description = "App tier"
vpc_id = aws_vpc.main.id
lifecycle { create_before_destroy = true }
}
# referenced_security_group_id で別 SG を「許可元」に指定できる
# → CIDR の代わりに「あの SG に属する EC2」を許可対象にする
resource "aws_vpc_security_group_ingress_rule" "app_from_web" {
security_group_id = aws_security_group.app.id
referenced_security_group_id = aws_security_group.web.id
from_port = 8080
to_port = 8080
ip_protocol = "tcp"
}
完成形
上で書いた要素を 1 ファイルに集約した main.tf 例(このサイト自身の Terraform リポジトリにも収録予定)。terraform apply で 2-AZ の Web 3 層 VPC が作れます。