再利用可能な Terraform モジュールの設計: 構造、I/O、およびレジストリ
実際のプロジェクトで Terraform を使用し始めると、正念場がやってきます。 HCL が 500 ~ 600 行になると、構成が読みにくくなり始めます。 同じパターンが異なる環境で繰り返され、リファクタリングが作業となる 危険です。解決策は私です Terraform モジュール: 再利用可能な抽象化ユニット 複雑さをカプセル化し、クリーンなインターフェイスを公開します。
このガイドでは、プロフェッショナルなフォームの作成方法だけでなく、フォームのデザインについても説明します。 しかしどうやって 考える パブリック インターフェイスへの移行、下位互換性の管理、 テストして Terraform レジストリに公開します。モジュールの設計が不十分であり、モジュールがないよりも悪い場合: 厳格な依存関係が作成され、アップグレードが困難になり、複雑さを管理する代わりに隠蔽します。
何を学ぶか
- プロフェッショナルな Terraform モジュールの正規構造
- 変数の設計: 型、検証、デフォルト値、および複雑なオブジェクト
- 標準化された出力: 何を表示するか、どの命名規則を使用するか
- 子モジュールと複合モジュール (構成と継承)
- セマンティック バージョニングと下位互換性管理
- Terraform パブリック レジストリおよびプライベート レジストリでの公開
- 高度なパターン: 一般的なフォーム、動的 for_each、条件付きフォーム
モジュールの正規構造
Terraform モジュールの構造は、レジストリと コミュニティが認識します。これらの規則から逸脱すると、フォームの利用者に混乱が生じます。
terraform-aws-networking/ # Naming: terraform-{provider}-{name}
├── main.tf # Logica principale del modulo
├── variables.tf # Input variables (interfaccia pubblica)
├── outputs.tf # Output values (interfaccia pubblica)
├── versions.tf # required_providers e terraform version
├── locals.tf # Valori computati interni
├── README.md # Documentazione (auto-generabile con terraform-docs)
├── CHANGELOG.md # Versioni e breaking changes
├── LICENSE # Apache 2.0 per moduli pubblici
├── examples/ # Esempi di utilizzo del modulo
│ ├── simple/
│ │ ├── main.tf # Uso minimo del modulo
│ │ └── outputs.tf
│ └── complete/
│ ├── main.tf # Uso completo con tutti i parametri
│ ├── variables.tf
│ └── outputs.tf
├── modules/ # Submoduli interni (opzionale)
│ └── subnet/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── test/ # Test del modulo
└── networking_test.go # Terratest
命名規則 terraform-{provider}-{name} そして必須です
パブリック Terraform レジストリで公開されており、プライベート レジストリにも推奨されます。
実際の例: terraform-aws-eks, terraform-google-kubernetes-engine,
terraform-azurerm-network.
変数設計: パブリック インターフェイス
モジュールの変数はそのパブリック API です。優れた可変設計 に続きます 最小の驚きの原則: モジュールのコンシューマ 変数の名前と説明を読むだけで何をすべきかを理解できるはずです。 内部コードを読まなくても。
# variables.tf — design professionale delle variabili
# Variabile richiesta (no default): rappresenta un input fondamentale
variable "vpc_cidr" {
description = "CIDR block per la VPC. Deve essere un CIDR /16 privato."
type = string
validation {
condition = can(regex(
"^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.)\\d+\\.\\d+/16$",
var.vpc_cidr
))
error_message = "Il CIDR deve essere un blocco privato /16 (RFC 1918)."
}
}
# Variabile con default ragionevole
variable "name" {
description = "Nome base usato per il prefisso di tutte le risorse."
type = string
default = "main"
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,28}[a-z0-9]$", var.name))
error_message = "Il nome deve essere lowercase, alfanumerico con trattini, 3-30 caratteri."
}
}
# Oggetto complesso: meglio di N variabili separate per configurazioni correlate
variable "nat_gateway_config" {
description = <<-EOT
Configurazione del NAT Gateway.
- enabled: crea il NAT Gateway (aggiunge costo ~$32/mese per AZ)
- single_az: usa un solo NAT Gateway (risparmio costi per non-prod)
EOT
type = object({
enabled = bool
single_az = bool
})
default = {
enabled = false
single_az = false
}
}
# Lista di oggetti: per configurazioni ripetute
variable "private_subnets" {
description = "Lista di subnet private da creare."
type = list(object({
cidr = string
availability_zone = string
tags = optional(map(string), {})
}))
default = []
validation {
condition = alltrue([
for s in var.private_subnets :
can(cidrhost(s.cidr, 0))
])
error_message = "Ogni subnet deve avere un CIDR valido."
}
}
# Variabile sensibile: non viene loggata nell'output di plan/apply
variable "database_password" {
description = "Password per il database. Usa Secrets Manager in produzione."
type = string
sensitive = true
validation {
condition = length(var.database_password) >= 16
error_message = "La password deve avere almeno 16 caratteri."
}
}
# Map per tag: pattern universale in Terraform
variable "tags" {
description = "Map di tag aggiuntivi da applicare a tutte le risorse."
type = map(string)
default = {}
}
# Feature flags: booleani che attivano/disattivano funzionalita
variable "enable_flow_logs" {
description = "Abilita VPC Flow Logs per il network monitoring."
type = bool
default = false
}
variable "enable_vpc_endpoints" {
description = "Crea VPC Endpoints per S3 e DynamoDB (riduce costi NAT)."
type = bool
default = true
}
内部ロジック: ローカルとリソース設計
モジュール内では、 locals それらは保持するための主要なツールです
DRYコード。すべての計算ロジックは分散せずにローカルに存在する必要があります
リソースブロック内。
# locals.tf — logica interna del modulo
locals {
# Naming convention centralizzata
name_prefix = var.name
# Merging tag: tag del modulo + tag dell'utente
# I tag dell'utente sovrascrivono i default del modulo
default_tags = {
ManagedBy = "Terraform"
Module = "terraform-aws-networking"
}
merged_tags = merge(local.default_tags, var.tags)
# Calcola automaticamente le AZ disponibili se non specificate
azs = length(var.private_subnets) > 0 ? [
for s in var.private_subnets : s.availability_zone
] : []
# Decisione sul NAT Gateway: uno per AZ o uno singolo
nat_count = var.nat_gateway_config.enabled ? (
var.nat_gateway_config.single_az ? 1 : length(local.azs)
) : 0
# Mappa per for_each: key univoca -> oggetto configurazione
private_subnets_map = {
for idx, subnet in var.private_subnets :
"${local.name_prefix}-private-${idx + 1}" => subnet
}
}
# main.tf — risorse del modulo
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-vpc"
})
}
# for_each su mappa: crea una risorsa per ogni elemento
resource "aws_subnet" "private" {
for_each = local.private_subnets_map
vpc_id = aws_vpc.this.id
cidr_block = each.value.cidr
availability_zone = each.value.availability_zone
tags = merge(
local.merged_tags,
each.value.tags,
{
Name = each.key
Tier = "Private"
}
)
}
# Risorsa condizionale: created solo se nat_gateway_config.enabled = true
resource "aws_eip" "nat" {
count = local.nat_count
domain = "vpc"
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-nat-eip-${count.index + 1}"
})
}
resource "aws_nat_gateway" "this" {
count = local.nat_count
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
depends_on = [aws_internet_gateway.this]
tags = merge(local.merged_tags, {
Name = "${local.name_prefix}-nat-${count.index + 1}"
})
}
# Flow Logs condizionali
resource "aws_cloudwatch_log_group" "vpc_flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc-flow-logs/${aws_vpc.this.id}"
retention_in_days = 30
tags = local.merged_tags
}
resource "aws_flow_log" "this" {
count = var.enable_flow_logs ? 1 : 0
vpc_id = aws_vpc.this.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn
}
アウトプット:何をどのように展示するか
モジュールの出力は変数と同じくらい重要です。それらはインターフェイスです。 親モジュールと他のモジュールが、作成されたリソースに関する情報を取得するために使用します。 黄金律は次のとおりです。 消費者が必要とするすべてのものを公開し、 しかし、それ以上はありません.
# outputs.tf — output standardizzati del modulo
# ID della VPC: sempre necessario per altri moduli
output "vpc_id" {
description = "ID della VPC creata."
value = aws_vpc.this.id
}
output "vpc_cidr" {
description = "CIDR block della VPC."
value = aws_vpc.this.cidr_block
}
# Liste di IDs: convenzione piu comune per subnet
output "private_subnet_ids" {
description = "Lista degli IDs delle subnet private."
value = [for s in aws_subnet.private : s.id]
}
# Map key->id: utile quando il consumatore deve referenziare subnet per nome
output "private_subnet_ids_by_name" {
description = "Map: nome subnet -> ID. Utile per for_each in moduli parent."
value = { for k, v in aws_subnet.private : k => v.id }
}
# ARN per policy IAM
output "vpc_arn" {
description = "ARN della VPC."
value = aws_vpc.this.arn
}
# Output condizionale: null se la risorsa non e stata creata
output "nat_gateway_ids" {
description = "IDs dei NAT Gateway. Lista vuota se nat_gateway_config.enabled = false."
value = aws_nat_gateway.this[*].id
}
# Output di un oggetto completo: utile per passare configurazione a moduli figli
output "vpc_config" {
description = "Configurazione completa della VPC per uso in moduli downstream."
value = {
id = aws_vpc.this.id
arn = aws_vpc.this.arn
cidr_block = aws_vpc.this.cidr_block
private_subnet_ids = [for s in aws_subnet.private : s.id]
nat_gateway_enabled = var.nat_gateway_config.enabled
}
}
モジュールの使用: 構文とバージョン管理
モジュールを使用する場合、Terraform はさまざまなソース (ローカル、Git、Terraform レジストリ) をサポートします。 予期しないリグレッションを回避するには、バージョン管理が不可欠です。
# Uso del modulo da sorgenti diverse
# 1. Modulo locale (sviluppo e test)
module "networking" {
source = "./modules/networking"
vpc_cidr = "10.0.0.0/16"
name = "dev"
}
# 2. Dal Terraform Registry pubblico con versione pinned
module "networking" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.1" # Permette 5.1.x ma non 5.2.0
name = "dev-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # Per dev/staging: risparmio costi
tags = {
Environment = "dev"
ManagedBy = "Terraform"
}
}
# 3. Da repository Git (registry privato aziendale)
module "networking" {
source = "git::https://github.com/myorg/terraform-modules.git//networking?ref=v2.3.1"
vpc_cidr = "10.0.0.0/16"
name = "prod"
nat_gateway_config = {
enabled = true
single_az = false # Multi-AZ in produzione
}
}
# 4. Da Terraform Enterprise o HCP Terraform Registry privato
module "networking" {
source = "app.terraform.io/myorg/networking/aws"
version = "~> 2.0"
vpc_cidr = "10.0.0.0/16"
}
# Accesso agli output del modulo
resource "aws_eks_cluster" "main" {
name = "prod-cluster"
role_arn = aws_iam_role.eks.arn
vpc_config {
# Referenzia l'output del modulo networking
subnet_ids = module.networking.private_subnet_ids
}
}
output "vpc_id" {
value = module.networking.vpc_id
}
モジュールのセマンティック バージョニング
Terraform モジュールは以下に従います。 セマンティック バージョニング (複数):
MAJOR.MINOR.PATCH。破壊的な変更と非破壊的な変更の違い
モジュールを使用する人に対する変更と批判。
# Cosa costituisce un breaking change (bump MAJOR):
# - Rimozione di una variabile obbligatoria
# - Cambiamento del tipo di una variabile esistente
# - Rimozione di un output
# - Rinomina di un output
# - Cambiamento del nome di una risorsa (causa destroy + recreate)
# - Aggiunta di una variabile obbligatoria senza default
# Non-breaking change (bump MINOR):
# - Aggiunta di una variabile opzionale (con default)
# - Aggiunta di un nuovo output
# - Aggiunta di una nuova funzionalita opzionale (feature flag)
# Patch:
# - Bugfix che non cambia l'interfaccia
# - Aggiornamento di versione di un sub-modulo
# - Miglioramenti alla documentazione
# Convenzioni nei version constraints:
# ~> 2.0 = >= 2.0, < 3.0 (piu usato: permette minor e patch)
# ~> 2.3 = >= 2.3, < 3.0
# ~> 2.3.0 = >= 2.3.0, < 2.4.0 (solo patch)
# >= 2.0, < 3.0 (equivalente a ~> 2.0 ma piu esplicito)
# CHANGELOG.md esempio:
# ## [3.0.0] - 2026-08-01
# ### Breaking Changes
# - Rimossa variabile `legacy_dns_mode` (deprecata dalla v2.5)
# - Output `subnet_id` rinominato in `private_subnet_ids` (lista)
#
# ## [2.5.0] - 2026-07-15
# ### Added
# - Aggiunto supporto VPC Endpoints per S3 e DynamoDB
# - Nuova variabile opzionale `enable_vpc_endpoints` (default: false)
# ### Deprecated
# - Variabile `legacy_dns_mode` sara rimossa nella v3.0
高度なパターン: 汎用モジュール
最も強力なモジュールは使用されるモジュールです for_each ダイナミックに作成する
完全にパラメトリックな構成。このパターンは、次のモジュールで一般的です。
同様のリソース セット (複数のサブネット、複数のセキュリティ グループ ルールなど) を管理します。
# Pattern: modulo per Security Groups completamente configurabile
# variables.tf del modulo sg
variable "security_groups" {
description = "Map di security groups da creare."
type = map(object({
description = string
ingress_rules = optional(list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string), [])
security_group_ids = optional(list(string), [])
})), [])
egress_rules = optional(list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string), ["0.0.0.0/0"])
})), [])
tags = optional(map(string), {})
}))
default = {}
}
# main.tf del modulo sg
resource "aws_security_group" "this" {
for_each = var.security_groups
name = "${var.name_prefix}-${each.key}"
description = each.value.description
vpc_id = var.vpc_id
tags = merge(var.tags, each.value.tags, {
Name = "${var.name_prefix}-${each.key}"
})
lifecycle {
create_before_destroy = true
}
}
# Flatten per le ingress rules: ogni SG puo avere N regole
locals {
ingress_rules = flatten([
for sg_name, sg_config in var.security_groups : [
for idx, rule in sg_config.ingress_rules : {
sg_name = sg_name
rule_idx = idx
rule = rule
}
]
])
}
resource "aws_security_group_rule" "ingress" {
for_each = {
for item in local.ingress_rules :
"${item.sg_name}-ingress-${item.rule_idx}" => item
}
type = "ingress"
security_group_id = aws_security_group.this[each.value.sg_name].id
description = each.value.rule.description
from_port = each.value.rule.from_port
to_port = each.value.rule.to_port
protocol = each.value.rule.protocol
cidr_blocks = each.value.rule.cidr_blocks
}
# Uso del modulo sg:
module "security_groups" {
source = "./modules/security-groups"
name_prefix = local.name_prefix
vpc_id = module.networking.vpc_id
security_groups = {
"web" = {
description = "Security group per istanze web"
ingress_rules = [
{
description = "HTTPS dal pubblico"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "HTTP redirect dal pubblico"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
"database" = {
description = "Security group per RDS"
ingress_rules = [
{
description = "PostgreSQL solo dal tier web"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_ids = [module.security_groups.ids["web"]]
cidr_blocks = []
}
]
}
}
}
terraform-docs による自動ドキュメント化
モジュールの変数と出力を手動で文書化するのは面倒で、時代遅れになることがよくあります。 テラフォームドキュメント HCL コードからドキュメントを自動的に生成します。
# Installa terraform-docs
brew install terraform-docs # macOS
# oppure
go install github.com/terraform-docs/terraform-docs@latest
# Genera README.md dal modulo
terraform-docs markdown table . > README.md
# Configurazione in .terraform-docs.yml
# formatter: "markdown table"
# output:
# file: README.md
# mode: inject # Inietta tra i marker nel README esistente
# settings:
# show-all: true
# indent: 2
# Con inject mode nel README.md:
#
# (documentazione auto-generata)
#
# Aggiorna il README automaticamente con pre-commit
# .pre-commit-config.yaml:
# - repo: https://github.com/terraform-docs/terraform-docs
# rev: "v0.19.0"
# hooks:
# - id: terraform-docs-go
# args: ["--output-file", "README.md", "--output-mode", "inject", "."]
Terraform レジストリに公開する
モジュールをパブリック Terraform レジストリに公開するには、GitHub リポジトリが必要です 特定の規則に従ってください。このプロセスは、GitHub タグを介してほぼ完全に自動化されています。
# Pre-requisiti per la pubblicazione:
# 1. Repository pubblico su GitHub
# 2. Nome repository: terraform-{provider}-{name}
# 3. Tag semver nel formato: v{MAJOR}.{MINOR}.{PATCH}
# Struttura obbligatoria per il Registry:
# main.tf, variables.tf, outputs.tf nella root
# README.md con documentazione
# Almeno un esempio in examples/
# Processo di release:
git tag -a v1.0.0 -m "Release v1.0.0: versione iniziale"
git push origin v1.0.0
# Il Registry riceve una webhook e indicizza automaticamente il modulo
# Per registry privato con HCP Terraform:
# 1. Vai su app.terraform.io -> Registry -> Publish Module
# 2. Connetti il repository GitHub
# 3. I tag vengono sincronizzati automaticamente
# terraform.tfvars per testing locale del modulo
vpc_cidr = "10.0.0.0/16"
name = "test"
nat_gateway_config = {
enabled = false
single_az = false
}
tags = {
Environment = "test"
Owner = "platform-team"
}
AWS に推奨されるオープンソース モジュール
モジュールを最初から作成する前に、成熟したバージョンがレジストリにすでに存在するかどうかを確認してください。 のモジュール terraform-aws-モジュール (アントン・バベンコ著) は事実上の標準です。
terraform-aws-modules/vpc/aws— 完全なネットワーキングterraform-aws-modules/eks/aws— EKSクラスターterraform-aws-modules/rds/aws— すべてのパラメータを含む RDSterraform-aws-modules/s3-bucket/aws— 暗号化とポリシーを備えた S3terraform-aws-modules/security-group/aws— 100 を超える事前設定されたルール
結論と次のステップ
優れた Terraform モジュールを設計するには、優れた API を設計するのと同じ注意が必要です。 消費者のことを考え、インターフェースを安定させ、すべてのパラメータを文書化します。 適切に設計されたモジュールは、チーム全体で何年も変更せずに使用されます。
Terraform の成熟度の次のステップは、状態管理に取り組むことです チーム内: 組織を拡大する際に最も頻繁に発生するリスクのある問題 Terraformを採用。
完全なシリーズ: Terraform と IaC
- 第01条 — ゼロからの Terraform: HCL、プロバイダー、およびプラン、適用、破棄
- 第02条(本) — 再利用可能な Terraform モジュールの設計: 構造、I/O、およびレジストリ
- 第03条 — Terraform 状態: S3/GCS を使用したリモート バックエンド、ロックおよびインポート
- 記事 04 — CI/CD の Terraform: GitHub アクション、Atlantis、およびプル リクエストのワークフロー
- 第 05 条 — IaC テスト: Terratest、Terraform ネイティブ テスト、および契約テスト
- 第 06 条 — IaC セキュリティ: Checkov、Trivy、OPA のポリシー・アズ・コード
- 記事 07 — Terraform マルチクラウド: 共有モジュールを使用した AWS + Azure + GCP
- 記事 08 — Terraform 用 GitOps: Flux TF コントローラー、スペースリフトおよびドリフト検出
- 記事 09 — Terraform vs Pulumi vs OpenTofu: 最終比較 2026
- 第 10 条 — Terraform エンタープライズ パターン: ワークスペース、センチネル、チーム スケーリング







