재사용 가능한 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-모듈 (Anton Babenko 작성)은 사실상의 표준입니다.
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 Actions, Atlantis 및 Pull Request Workflow
- 조항 05 — IaC 테스트: Terratest, Terraform 기본 테스트 및 계약 테스트
- 06조 — IaC 보안: Checkov, Trivy 및 OPA Policy-as-Code
- 기사 07 — Terraform 다중 클라우드: AWS + Azure + 공유 모듈이 있는 GCP
- 기사 08 — Terraform용 GitOps: Flux TF 컨트롤러, Spacelift 및 드리프트 감지
- 기사 09 — Terraform vs Pulumi vs OpenTofu: 2026년 최종 비교
- 기사 10 — Terraform 엔터프라이즈 패턴: 작업 공간, Sentinel 및 팀 확장







