【第2回】Terraform × AWS インフラ自動化入門 — VPC・EC2・RDSをコードで構築するリソース定義の書き方

テックツール

前回は Terraform の概要・インストール・terraform init / plan / apply の基本的な流れを紹介しました(第1回はこちら)。今回はいよいよ実際のリソースを定義していきます。VPC・EC2・RDS という典型的な構成を題材に、HCL の書き方とモジュール分割の考え方を整理します。

この記事でわかること

  • Terraform モジュールの基本的なディレクトリ構成
  • VPC・サブネット・インターネットゲートウェイの定義方法
  • EC2 インスタンスの定義とセキュリティグループの設定
  • RDS インスタンスの定義方法
  • 複数モジュールを組み合わせて AWS インフラを構築する方法
  • モジュール分割のプラクティス

このシリーズについて

Terraform は 2026年4月にも新しいバージョンがリリースされるなど、継続的に進化しています。このシリーズでは実務的な使い方を意識しながら進めていくので、バージョンアップにも対応しやすくなるはずです。

目指すディレクトリ構成

以下のようなモジュール構成を目指します。

terraform-aws/
├── main.tf          # ルートモジュール(各モジュールを呼び出す)
├── variables.tf
├── outputs.tf
├── terraform.tfvars
└── modules/
    ├── vpc/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── ec2/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── rds/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

「なんでいきなりモジュール分割?」と思うかもしれませんが、最初から分けておく習慣をつけると、後から環境分離やチーム開発に対応しやすくなります。経験談です。

VPC モジュール — ネットワーク基盤を定義する

まず modules/vpc/main.tf から。VPC・パブリックサブネット・プライベートサブネット・インターネットゲートウェイ・ルートテーブルをまとめて定義します。

# modules/vpc/main.tf

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true

  tags = {
    Name = "${var.name}-vpc"
  }
}

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.name}-public-${count.index + 1}"
  }
}

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.name}-private-${count.index + 1}"
  }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = {
    Name = "${var.name}-igw"
  }
}

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.name}-public-rt"
  }
}

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
}

count を使うとサブネットを複数まとめて作れます。AZ を2つ指定すれば2つ分できる、というシンプルな仕組みです。

次に変数定義です。

# modules/vpc/variables.tf

variable "name" {
  type = string
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  type    = list(string)
  default = ["10.0.11.0/24", "10.0.12.0/24"]
}

variable "availability_zones" {
  type    = list(string)
  default = ["ap-northeast-1a", "ap-northeast-1c"]
}

そして出力定義。

# modules/vpc/outputs.tf

output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

outputs.tf で VPC ID とサブネット ID を外に出しておくのが重要です。後続の EC2・RDS モジュールからここを参照します。

EC2 モジュール — コンピュートリソースの定義

セキュリティグループと EC2 インスタンスをセットで定義します。

# modules/ec2/main.tf

resource "aws_security_group" "this" {
  name   = "${var.name}-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # ここ注意: 本番では絞ること
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.name}-sg"
  }
}

resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.this.id]
  key_name               = var.key_name

  tags = {
    Name = "${var.name}-ec2"
  }
}
# modules/ec2/variables.tf

variable "name" {
  type = string
}

variable "vpc_id" {
  type = string
}

variable "subnet_id" {
  type = string
}

variable "ami_id" {
  type    = string
  default = "ami-0d52744d6551d851e"
}

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

variable "key_name" {
  type    = string
  default = null
}

AMI ID はデフォルト値を入れていますが、リージョンや時期によって変わるので data "aws_ami" で動的に取得するほうがより安全です。

# modules/ec2/outputs.tf

output "instance_id" {
  value = aws_instance.this.id
}

output "public_ip" {
  value = aws_instance.this.public_ip
}

output "security_group_id" {
  value = aws_security_group.this.id
}

RDS モジュール — データベース基盤の構築

RDS はサブネットグループとセキュリティグループがセットで必要なので、ちょっとだけ行数が増えます。

# modules/rds/main.tf

resource "aws_db_subnet_group" "this" {
  name       = "${var.name}-subnet-group"
  subnet_ids = var.subnet_ids

  tags = {
    Name = "${var.name}-subnet-group"
  }
}

resource "aws_security_group" "this" {
  name   = "${var.name}-rds-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [var.ec2_security_group_id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.name}-rds-sg"
  }
}

resource "aws_db_instance" "this" {
  identifier        = "${var.name}-db"
  engine            = "mysql"
  engine_version    = "8.0"
  instance_class    = var.instance_class
  allocated_storage = var.allocated_storage

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.this.name
  vpc_security_group_ids = [aws_security_group.this.id]

  skip_final_snapshot = true # ここ注意: 本番では false に

  tags = {
    Name = "${var.name}-db"
  }
}
# modules/rds/variables.tf

variable "name" {}
variable "vpc_id" {}
variable "subnet_ids" {
  type = list(string)
}
variable "ec2_security_group_id" {}

variable "instance_class" {
  default = "db.t3.micro"
}

variable "allocated_storage" {
  type    = number
  default = 20
}

variable "db_name" {
  default = "appdb"
}

variable "db_username" {}
variable "db_password" {
  sensitive = true
}

db_password には sensitive = true をつけておくと、terraform plan の出力でマスクされます。地味に大事な設定です。

# modules/rds/outputs.tf

output "endpoint" {
  value = aws_db_instance.this.endpoint
}

ルートモジュール — 全モジュールの統合

3つのモジュールができたので、ルート直下の main.tf から呼び出します。

# main.tf

terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

module "vpc" {
  source = "./modules/vpc"

  name                 = var.project_name
  vpc_cidr             = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
  availability_zones   = ["ap-northeast-1a", "ap-northeast-1c"]
}

module "ec2" {
  source = "./modules/ec2"

  name      = var.project_name
  vpc_id    = module.vpc.vpc_id
  subnet_id = module.vpc.public_subnet_ids[0]
}

module "rds" {
  source = "./modules/rds"

  name                  = var.project_name
  vpc_id                = module.vpc.vpc_id
  subnet_ids            = module.vpc.private_subnet_ids
  ec2_security_group_id = module.ec2.security_group_id
  db_username           = var.db_username
  db_password           = var.db_password
}

module.vpc.vpc_id のように、モジュールの outputs を他のモジュールの inputs として渡せるのがポイントです。依存関係を明示的に書くことで、Terraform が apply の順番を自動的に解決してくれます。

# variables.tf

variable "aws_region" {
  default = "ap-northeast-1"
}

variable "project_name" {
  default = "my-app"
}

variable "db_username" {}
variable "db_password" {
  sensitive = true
}
# terraform.tfvars(.gitignore に追加推奨)

project_name = "my-app"
db_username  = "admin"
db_password  = "changeme-please"

実際に構築する — コマンド実行の流れ

ファイルが揃ったら以下の順で実行します。

# モジュールを読み込む(初回 or modules/ を変えたとき)
terraform init

# 差分確認
terraform plan

# 実際に構築
terraform apply

terraform plan の出力でリソース名が module.vpc.aws_vpc.this のように表示されていれば、モジュールが正しく読み込めています。逆にここでエラーが出るときは変数名のタイポか source パスのミスが多いです。自分は最初 ./modules/vpcmodules/vpc(先頭の ./ なし)で書いてエラーを出しました。これは環境次第では動くこともあるようですが、念のため ./ つきで書く方が無難です。

壊すときは terraform destroy で。RDS は削除に数分かかるので気長に待ちましょう。

モジュール分割の粒度 — 実装のポイント

今回は vpc / ec2 / rds と「サービス単位」で分けましたが、これが絶対正解というわけではありません。Terraform のモジュール構成には標準的なアプローチはいくつかありますが、チーム規模やプロジェクトの成熟度に応じて柔軟に選ぶことが重要です。

個人的に意識しているのは、「1つのモジュールが何のためのものか、名前を見ればわかる」 かどうか、という点だけです。そこさえ守れば、最初は粗くても後からリファクタリングできます。

あと、ネストしたモジュール(モジュールがさらにモジュールを呼ぶ)は深くなるほど依存関係が複雑になるため、モジュールをフラットに並べる構成が推奨されています。正直なところ、ここを深く理解するには実装経験が必要だと感じるので、まずはフラットな構成で書いていく方針で十分です。

まとめ

  • Terraform モジュールは ./modules/ ディレクトリ配下に分割配置するのが定石
  • VPC・EC2・RDS など、責務ごとにモジュールを分けることで保守性が向上
  • outputs.tf で必要な値を外に出し、他のモジュールから module.xxx.yyy で参照
  • count を使うことで複数リソースをループ処理できる
  • 本番環境では skip_final_snapshot や セキュリティグループの CIDR 制限に注意が必要
  • 最初からモジュール分割する習慣をつけると、後の拡張が容易

📚 シリーズ「Terraform × AWS インフラ自動化入門」(第2回 / 全4回)

← 前回の記事: 前回の記事はこちら

→ 次回の記事: 【第3回】Terraform × AWS インフラ自動化入門 — モジュール化とワークスペースで複数環境(dev/stg/prod)を管理する

参考になったらクリックしてもらえると嬉しいです!

Blogmura NetDev Ranking
タイトルとURLをコピーしました