★★ 中級

02. VPC とネットワーク

AWS のほぼ全リソースは VPC(仮想ネットワーク) の上で動きます。最初の VPC を作り、パブリック/プライベートのサブネットに分け、インターネットと出入りできるようにルーティングする。これが AWS インフラの「土台 50%」です。

登場人物の早見表

用語役割たとえると
VPCAWS 上のあなた専用の仮想ネットワークマンション全体の専有エリア
SubnetVPC を切り分けた区画。1 つの AZ に紐づくマンションの各フロア
Internet Gateway (IGW)VPC をインターネットにつなぐ玄関建物のメイン出入口
Route Table「このサブネットの通信は、ここに送る」のルール表各部屋の郵便配送ルール
NAT Gatewayプライベートサブネットの「外向きだけ」通信を可能にする外には出られるが、外からは入れない通用口
Elastic IP (EIP)固定のグローバル IP アドレスNAT Gateway 等に貼り付ける番地
Security Group (SG)リソース単位のファイアウォール各部屋のドアの鍵

標準トポロジ図

2 つの AZ にまたがる、Web 3 層構成の標準図です。

VPC + 2 AZ + Public/Private/DB Subnet + IGW + NAT GW のトポロジー
Public Subnet は IGW にルートを持つ。Private Subnet は NAT GW 経由で外向き通信。DB Subnet は外部ルートなし。

ポイントは、サブネットの種類はネットワーク的に決まる こと。「パブリックサブネット」とは、ルートテーブルが 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 を参照する
arnIAM ポリシー条件に書く
default_security_group_idデフォルト SG。使わない のが原則
main_route_table_idVPC のメインルートテーブル

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 が作れます。