moduleブロック — リソースのモジュール化と再利用

1. 概要

この記事では、以下の内容を解説します。

  • moduleブロックの基本構文とモジュールの呼び出し方
  • モジュールのディレクトリ構成(variables.tf / main.tf / outputs.tf)
  • variableでモジュールに値を渡す方法
  • outputでモジュールの値を親から参照する方法(module.<NAME>.<OUTPUT>
  • for_eachを使ってモジュールを複数インスタンス化する
  • Terraform Registryの公開モジュールを使う
  • よくあるエラーと対処法

moduleは、関連するリソース群をひとまとめにして再利用可能にする仕組みです。EC2・RDS・VPCなど、セットで使うリソースをモジュール化することで、コードの重複を排除し、環境間(dev/stg/prd)での一貫性を保てます。


2. moduleとは

Terraformのコードはモジュールという単位で管理します。ルートモジュール(terraform applyを実行するディレクトリ)から、サブモジュール(./modules/配下のディレクトリ)を呼び出す構成が基本です。

project/
├── main.tf          ← ルートモジュール(moduleブロックを書く)
├── variables.tf
├── outputs.tf
└── modules/
    ├── vpc/         ← サブモジュール
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── ec2/         ← サブモジュール
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

3. 基本構文

module "<モジュール名>" {
  source = "<モジュールのパス>"

  # サブモジュールのvariableに値を渡す
  <variable名> = <値>
}

シンプルな例

# ルートモジュール(main.tf)
terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "vpc" {
  source = "./modules/vpc"  # 相対パスでモジュールを指定

  project     = var.project
  environment = var.environment
  cidr_block  = "10.0.0.0/16"
}

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

  project     = var.project
  environment = var.environment
  vpc_id      = module.vpc.vpc_id              # VPCモジュールの出力を参照
  subnet_ids  = module.vpc.private_subnet_ids  # VPCモジュールの出力を参照
}

4. モジュールの内部構成

サブモジュールの標準ファイル構成

# modules/ec2/variables.tf
variable "project" {
  description = "プロジェクト名(リソース名プレフィックス用)"
  type        = string
}

variable "environment" {
  description = "環境名(dev/stg/prd)"
  type        = string
}

variable "vpc_id" {
  description = "EC2を配置するVPCのID"
  type        = string
}

variable "subnet_ids" {
  description = "EC2を配置するサブネットIDのリスト"
  type        = list(string)
}

variable "instance_type" {
  description = "EC2インスタンスタイプ"
  type        = string
  default     = "t3.micro"
}
# modules/ec2/main.tf
locals {
  name_prefix = "${var.project}-${var.environment}"
}

data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = var.instance_type
  subnet_id     = var.subnet_ids[0]

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
    encrypted   = true
  }

  tags = {
    Name        = "${local.name_prefix}-app"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
# modules/ec2/outputs.tf
output "instance_id" {
  description = "作成したEC2インスタンスのID"
  value       = aws_instance.app.id
}

output "private_ip" {
  description = "EC2インスタンスのプライベートIPアドレス"
  value       = aws_instance.app.private_ip
}

5. moduleのoutputを参照する

サブモジュールのoutputは module.<モジュール名>.<output名> で参照します。

# ルートモジュールでモジュールのoutputを参照
output "app_instance_id" {
  description = "アプリサーバーのインスタンスID"
  value       = module.ec2.instance_id  # module.<NAME>.<OUTPUT_NAME>
}

output "app_private_ip" {
  description = "アプリサーバーのプライベートIP"
  value       = module.ec2.private_ip
}

# 別のリソースでもモジュールのoutputを使える
resource "aws_route53_record" "app" {
  zone_id = var.hosted_zone_id
  name    = "app.example.com"
  type    = "A"
  ttl     = 300
  records = [module.ec2.private_ip]
}

6. for_eachでモジュールを複数インスタンス化する

for_eachをmoduleブロックに使うと、モジュールを複数環境分まとめて作成できます。

variable "environments" {
  description = "デプロイする環境の設定"
  type = map(object({
    instance_type = string
    cidr_block    = string
  }))
  default = {
    dev = { instance_type = "t3.micro",  cidr_block = "10.0.0.0/16" }
    stg = { instance_type = "t3.small",  cidr_block = "10.1.0.0/16" }
    prd = { instance_type = "t3.medium", cidr_block = "10.2.0.0/16" }
  }
}

module "vpc" {
  for_each = var.environments  # dev/stg/prd の3つのVPCを作成

  source = "./modules/vpc"

  project       = var.project
  environment   = each.key          # "dev" / "stg" / "prd"
  cidr_block    = each.value.cidr_block
}

# for_eachモジュールのoutputはmapで返ってくる
output "vpc_ids" {
  description = "各環境のVPC ID"
  value = {
    for env, mod in module.vpc : env => mod.vpc_id
  }
  # → { dev = "vpc-abc", stg = "vpc-def", prd = "vpc-ghi" }
}

7. Terraform Registryの公開モジュールを使う

sourceにレジストリのパスを指定すると、Terraform Registryの公開モジュールを利用できます。

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"  # レジストリのモジュール
  version = "~> 4.0"                               # バージョン固定(必須)

  bucket = "my-project-bucket"
  acl    = "private"

  versioning = {
    enabled = true
  }

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

💡 注意: 公開モジュールを使う場合は必ずversionを指定してください。指定しないとモジュールの破壊的変更が自動的に取り込まれるリスクがあります。

# 公開モジュールは terraform init でダウンロードされる
terraform init

8. よくあるエラー

Error: Module not installed

│ Error: Module not installed
│
│ This module is not yet installed. Run "terraform init" to install all modules
│ required by this configuration.

原因: moduleブロックを追加・変更したのにterraform initを実行していない。

解決方法:

terraform init

Error: Unsupported argument

│ Error: Unsupported argument
│
│ An argument named "instance_count" is not expected here.

原因: モジュールに存在しないvariable名を渡している。

解決方法: モジュールのvariables.tfを確認し、正しい変数名を使う。

モジュールのoutputを参照できない

│ Error: Unsupported attribute
│
│ This object does not have an attribute named "private_ip".

原因: モジュールのoutputs.tfprivate_ipが定義されていない。

解決方法: サブモジュールのoutputs.tfに必要なoutputを追加する。


9. 関連記事


10. まとめ

  • moduleブロックでsourceにパスを指定してサブモジュールを呼び出す
  • サブモジュールはvariables.tf(入力) / main.tf(リソース) / outputs.tf(出力)の3ファイルで構成
  • 値の受け渡し: 親→子はvariable経由、子→親はmodule.<NAME>.<OUTPUT_NAME>で参照
  • for_eachをmoduleブロックに使うと複数環境分のリソースをまとめて作成できる
  • Terraform Registryの公開モジュールはsourceにレジストリパスを指定し、versionを必ず固定する
  • moduleを追加・変更したら必ずterraform initを実行すること

動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/modules