はじめに
Terraformで再利用可能なVPCモジュールを構築する手順を、2回に分けて解説します。
- 第1回(本記事): 全体像とネットワーク基盤(VPC・サブネット・ルーティング・NAT Gateway)
- 第2回: セキュリティグループ・監査編(デフォルトSGの制限、VPC Flow Logs)
第1回では、まずこれから作るネットワークの全体像と、サブネットに何を配置するかの前提を共有した上で、実際に通信が成立する骨格部分をTerraformコードで組み立てます。
配置の前提
サブネットの設計は「何を置くか」によって決まります。本シリーズでは、以下の構成を前提とします。
| サブネット | 配置するもの | 理由 |
|---|---|---|
| パブリックサブネット | ALB、NAT Gateway | インターネットからの入口・出口として直接通信が必要 |
| プライベートサブネット | バックエンドAPI(ECS)、フロントエンド Next.js(ECS)、RDS | インターネットから直接アクセスさせず、ALB経由でのみ到達させたいリソース |
フロントエンド(Next.js)とバックエンドAPIはどちらもコンテナとしてプライベートサブネットに置き、ALBがパスなどに応じて振り分ける構成を想定しています。RDSも直接インターネットからは到達不可な状態で、プライベートサブネットのみに配置します。
全体像
配置の前提を踏まえた全体像は以下の通りです。
Internet
│
[Internet Gateway]
│
┌────────────────────────────── VPC ───────────────────────────────┐
│ │
│ AZ-a AZ-c │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ Public Subnet │ │ Public Subnet │ │
│ │ [ALB] [NAT Gateway] │ │ [ALB] │ │
│ └───────────┬────────────┘ └───────────┬────────────┘ │
│ │ 0.0.0.0/0 → NAT GW │ │
│ ┌───────────┴────────────┐ ┌────────────┴──────────┐ │
│ │ Private Subnet │ │ Private Subnet │ │
│ │ [ECS: バックエンドAPI] │ │ [ECS: バックエンドAPI] │ │
│ │ [ECS: Next.js] │ │ [ECS: Next.js] │ │
│ │ [RDS] │ │ [RDS: スタンバイ] │ │
│ └─────────────────────────┘ └───────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
ポイントは3つです。
- サブネットは「パブリック」と「プライベート」の2種類に分ける。ALBやNAT Gatewayのような「外部との窓口」だけをパブリックサブネットに置き、バックエンドAPI・Next.js・RDSといったアプリケーション本体はプライベートサブネットに置きます。
- サブネットは複数のAZ(アベイラビリティゾーン)に分散配置する。1つのAZだけに集めると、そのAZに障害が起きたときにサービス全体が止まってしまうためです。
- 「行き」の通信経路をルートテーブルで制御する。パブリックサブネットはインターネットゲートウェイ(IGW)経由で直接インターネットへ、プライベートサブネットはNATゲートウェイを経由して間接的にインターネットへ出ます(バックエンドAPIやNext.jsコンテナが外部APIを呼んだり、パッケージを取得したりするために必要)。
なお、本記事ではVPCモジュール(ネットワークの骨格)のみを対象とし、ALB・ECS・RDS自体の構築は扱いません。「これらが将来乗ることを見越したサブネット設計」という位置づけで進めます。
対象リソースは以下の10種類です。
| # | リソースタイプ | ローカル名 | 役割 |
|---|---|---|---|
| 1 | aws_vpc |
this |
VPC本体 |
| 2 | aws_internet_gateway |
this |
インターネットゲートウェイ |
| 3 | aws_subnet |
public(count) |
パブリックサブネット |
| 4 | aws_route_table |
public |
パブリック用ルートテーブル |
| 5 | aws_route_table_association |
public(count) |
パブリックサブネットの紐付け |
| 6 | aws_subnet |
private(count) |
プライベートサブネット |
| 7 | aws_eip |
nat |
NAT Gateway用EIP |
| 8 | aws_nat_gateway |
this |
NATゲートウェイ |
| 9 | aws_route_table |
private |
プライベート用ルートテーブル |
| 10 | aws_route_table_association |
private(count) |
プライベートサブネットの紐付け |
セキュリティグループ(デフォルトSG)とVPC Flow Logsは第2回で扱います。
ステップ1: VPCとインターネットゲートウェイを作る
すべての土台となるVPC本体を作成します。DNS解決とDNSホスト名の付与を有効化しておかないと、後々ECSやRDSなどでプライベートDNSを使う際につまずくため、最初から有効にしておきます。
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.project}-${var.env}-vpc"
}
}
VPCを外部(インターネット)とつなぐための出入り口として、インターネットゲートウェイ(IGW)を1つ作成し、VPCにアタッチします。IGWはVPCに1つあれば十分で、後述するパブリックサブネットのルートテーブルから参照されます。
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${var.project}-${var.env}-igw"
}
}
ステップ2: サブネットをAZに分散して作る
パブリック/プライベートの2種類のサブネットを、複数のAZに分散して作成します。CIDRとAZのリストをcountで回すことで、AZ数を可変にできます。
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-${var.env}-public-${count.index + 1}"
Tier = "public"
}
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project}-${var.env}-private-${count.index + 1}"
Tier = "private"
}
}
パブリックサブネットにはmap_public_ip_on_launch = trueを指定しています。これにより、このサブネットに起動したリソース(ALBなど)へ自動でパブリックIPが割り当てられます。プライベートサブネットにはこの指定をしないため、パブリックIPは付与されません。
対応する変数定義は以下の通りです。public_subnet_cidrs・private_subnet_cidrs・availability_zonesは要素数を揃えて渡す前提のリストです。例えば2AZ構成なら、それぞれ2要素のリストを渡します。
variable "project" { type = string }
variable "env" { type = string }
variable "vpc_cidr" { type = string }
variable "public_subnet_cidrs" { type = list(string) }
variable "private_subnet_cidrs" { type = list(string) }
variable "availability_zones" { type = list(string) }
ステップ3: パブリックサブネットの経路をIGWに向ける
サブネットを作っただけでは、まだどこにも通信できません。ルートテーブルで「どこ宛の通信をどこへ送るか」を定義する必要があります。パブリックサブネット用のルートテーブルは、0.0.0.0/0(すべての宛先)をIGWへ向けます。これにより、パブリックサブネットに置かれたALBはインターネットと直接通信できるようになります。
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = {
Name = "${var.project}-${var.env}-rtb-public"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
aws_route_table_associationは、作成したルートテーブルとサブネットを紐付けるためのリソースです。これを行って初めて、そのサブネットにそのルートテーブルの経路が適用されます。
ステップ4: プライベートサブネットの経路をNAT Gateway経由でインターネットに向ける
プライベートサブネットはIGWに直接つなぎません。代わりに、パブリックサブネット上にNATゲートウェイを置き、そこを経由して間接的にインターネットへアクセスできるようにします。これにより、外部からバックエンドAPI・Next.js・RDSへ直接アクセスすることはできない一方、これらのリソース側からの通信(パッケージのダウンロードや外部API呼び出しなど)は可能になります。
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.project}-${var.env}-eip-nat"
}
}
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.project}-${var.env}-natgw"
}
depends_on = [aws_internet_gateway.this]
}
aws_nat_gateway.thisのsubnet_idがaws_subnet.public[0].id固定になっている点に注目してください。これは、AZ冗長化をせず1台のNATゲートウェイのみを作成する構成です。EIPとNATゲートウェイの時間課金を抑えられる一方、そのAZに障害が起きると全プライベートサブネットの外部通信が止まるというトレードオフがあります。可用性を優先する場合は、この部分をcount = length(var.availability_zones)に変更し、EIPとNATゲートウェイをAZ数分作成した上で、後述のプライベート用ルートテーブルもAZごとに分離する構成に拡張します。
プライベートサブネット用のルートテーブルは、0.0.0.0/0をこのNATゲートウェイへ向けます。
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this.id
}
tags = {
Name = "${var.project}-${var.env}-rtb-private"
}
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
ここまでで、全体像の図で示した「通信が成立するネットワークの骨格」が完成しました。ALB・ECS(バックエンドAPI/Next.js)・RDSといった実際のワークロードは、このVPCモジュールが公開するサブネットIDを受け取って、それぞれ別モジュールとして構築していく形になります。
まとめ
- VPCとIGWを作る(土台)
- パブリック/プライベートサブネットをAZに分散して作る(区画分け・配置の前提に基づく設計)
- パブリックサブネットの経路をIGWに向ける(ALBの入出口)
- プライベートサブネットの経路をNAT Gateway経由でインターネットに向ける(バックエンドAPI・Next.js・RDSの出口)
次回では、VPC作成時に自動生成される全許可のデフォルトセキュリティグループを制限する設計と、VPC内の通信を可視化するVPC Flow Logsの設定、モジュールのoutputsによる他モジュールとの連携設計を解説します。