★★ 中級

08. モジュール

同じパターンの resource 群を別ディレクトリに切り出して、入力(variable)と出力(output)だけを公開する仕組みが モジュール。プログラミングの「関数」と同じ役割で、再利用とテストの単位になります。

モジュールとは

Terraform で「モジュール」とは 1 つのディレクトリ を指します。.tf が並んでいれば、もうそれは root モジュール。別ディレクトリを module "..." ブロックで呼び出せば、それは 子モジュール です。

my-infra/
├── main.tf            # ← root モジュール
├── variables.tf
├── outputs.tf
└── modules/
    └── network/       # ← 子モジュール
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

呼び出し方

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

  vpc_cidr            = "10.0.0.0/16"
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]

  tags = {
    Project = "hcl-guide"
    Env     = "dev"
  }
}

# 子モジュールの output は module.<NAME>.<OUTPUT> で参照
resource "aws_instance" "web" {
  subnet_id              = module.network.public_subnet_ids[0]
  vpc_security_group_ids = [module.network.web_sg_id]
  # ...
}

source の書き方

形式用途
ローカルパス"./modules/network"同じリポジトリ内
Terraform Registry"hashicorp/consul/aws"公開モジュール、version 必須
Git (HTTPS)"git::https://github.com/org/mod.git//path?ref=v1.2.0"社内 Git 等
Git (SSH)"git::ssh://git@github.com/org/mod.git?ref=v1.2.0"SSH 認証
S3 / GCS"s3::https://bucket.s3-region.amazonaws.com/mod.zip"アーカイブ配布

Registry 経由のときは version で SemVer の制約を付けます。

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.5"

  name = "main"
  cidr = "10.0.0.0/16"
  azs  = ["ap-northeast-1a", "ap-northeast-1c"]
  # ...
}
本番では必ず固定 version = "~> 5.5" は「5.5.x の最新」。本番運用では具体バージョンに固定するか、.terraform.lock.hcl で完全固定(こちらは terraform init 時に自動生成)。

入力(variable)と出力(output)の設計

子モジュールは 外との接点variableoutput だけに絞るのが原則。中身(main.tf)は呼び出し側に見せない設計を心がける。

modules/network/variables.tf

variable "vpc_cidr" {
  type        = string
  description = "VPC の CIDR ブロック"

  validation {
    condition     = can(cidrnetmask(var.vpc_cidr))
    error_message = "vpc_cidr は有効な CIDR でなければなりません。"
  }
}

variable "public_subnet_cidrs" {
  type        = list(string)
  description = "パブリックサブネットの CIDR ブロック一覧"
}

variable "tags" {
  type        = map(string)
  default     = {}
  description = "全リソースに付ける共通タグ"
}

modules/network/outputs.tf

output "vpc_id" {
  description = "作成した VPC の ID"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "パブリックサブネットの ID 一覧"
  value       = [for s in aws_subnet.public : s.id]
}

output "web_sg_id" {
  description = "Web サーバ用セキュリティグループの ID"
  value       = aws_security_group.web.id
}

count / for_each をモジュールに付ける

module ブロックそのものに countfor_each を付けて、複数の環境を 1 つの root から作れます。

module "service" {
  source = "./modules/service"
  for_each = {
    api    = { cpu = 256,  memory = 512  }
    worker = { cpu = 1024, memory = 2048 }
  }

  name   = each.key
  cpu    = each.value.cpu
  memory = each.value.memory
}

# 参照
output "api_dns" {
  value = module.service["api"].dns_name
}

複数プロバイダを子モジュールに渡す

たとえば「ACM 証明書は us-east-1 で作る」「他は ap-northeast-1」のように、リージョンごとに別 provider を使い分ける場合。子モジュール側で provider を宣言してはいけません(公式推奨)。代わりに configuration_aliases で「受け取り口」を作って、親から providers 引数で渡します。

# 親 (root)
provider "aws" {
  alias  = "tokyo"
  region = "ap-northeast-1"
}
provider "aws" {
  alias  = "virginia"
  region = "us-east-1"
}

module "site" {
  source = "./modules/static-site"

  providers = {
    aws       = aws.tokyo       # デフォルト
    aws.acm   = aws.virginia    # ACM 用
  }

  domain_name = "hcl-guide.com"
}
# 子 modules/static-site/terraform.tf
terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = "~> 5.0"
      configuration_aliases = [aws.acm]   # ← 受け取り口を宣言
    }
  }
}

# 中で使う
resource "aws_acm_certificate" "this" {
  provider          = aws.acm
  domain_name       = var.domain_name
  validation_method = "DNS"
}

良いモジュール/悪いモジュール

良い悪い
変数が「呼び出し側の関心」だけ(CIDR、サイズ等)「内部実装の引数」が外に漏れている(インスタンスの内部タグ命名規則とか)
output が「次のモジュールが必要としそうな ID」output が無い/全 attribute をそのまま流すだけ
README で「何のための/どう使う」が 5 行以内に書ける使い方が読まないとわからない
1 つの責務(VPC、IAM、ECS サービス等)「全部入りの巨大モジュール」
環境差はモジュール外(呼び出し側)に出すモジュール内に if env == "prd" が散在