Terraform Modules

Terraform 모듈이란: Root Module과 Child Module의 관계

Terraform에서 모듈은 함께 관리되는 리소스의 집합입니다. 모든 Terraform 구성은 그 자체로 모듈이며, 작업 디렉터리의 .tf 파일들이 root module입니다. root module에서 module 블록으로 호출하는 모듈이 child module이고, child module은 다시 자체적으로 child module을 호출할 수 있는 계층 구조를 형성합니다.

# root module에서 child module 호출
module "vpc" {
  source  = "./modules/vpc"
  
  cidr_block = "10.0.0.0/16"
  azs        = ["ap-northeast-2a", "ap-northeast-2c"]
}

module "eks" {
  source  = "./modules/eks"
  
  vpc_id     = module.vpc.vpc_id        # 모듈 간 의존성
  subnet_ids = module.vpc.private_subnet_ids
}

이 구조에서 핵심은 모듈 간 통신이 오직 input variables(variables.tf)와 output values(outputs.tf)로만 이루어진다는 점입니다. 모듈 내부의 리소스에 직접 접근할 수 없으며, 이것이 모듈의 캡슐화를 보장합니다.

모듈 디렉터리 구조: 표준 파일 레이아웃

Terraform 공식 문서에서 권장하는 모듈의 표준 구조는 다음과 같습니다:

modules/
└── vpc/
    ├── main.tf          # 핵심 리소스 정의
    ├── variables.tf     # input variables (모듈의 인터페이스)
    ├── outputs.tf       # output values (다른 모듈에 노출)
    ├── versions.tf      # required_providers, terraform 버전 제약
    ├── locals.tf        # 내부 계산값 (선택)
    └── README.md        # 사용법 문서
파일 역할 필수 여부
main.tf 리소스·data source 정의 필수
variables.tf 모듈의 입력 인터페이스 필수
outputs.tf 모듈의 출력 인터페이스 필수
versions.tf provider·Terraform 버전 제약 권장
README.md 사용법·예제·input/output 설명 공개 모듈 필수

규칙: 모듈은 하나의 논리적 인프라 단위를 표현해야 합니다. “VPC”, “EKS 클러스터”, “RDS 인스턴스”처럼 명확한 경계가 있어야 하며, “전체 인프라”를 하나의 모듈에 넣는 것은 안티패턴입니다.

모듈 소스(source): 로컬·레지스트리·Git 참조

source 인수는 모듈을 어디서 가져올지 결정합니다. Terraform은 다양한 소스를 지원합니다:

소스 타입 source 문법 버전 관리 적합한 상황
로컬 경로 "./modules/vpc" 없음 (항상 최신) 같은 리포지터리 내 모듈
Terraform Registry "hashicorp/consul/aws" version = "~> 0.1" 공개/사내 레지스트리 모듈
GitHub "github.com/org/repo//modules/vpc" ?ref=v1.2.0 사내 Git 저장소
Generic Git "git::https://example.com/repo.git" ?ref=tag 자체 호스팅 Git
S3 Bucket "s3::https://bucket.s3.amazonaws.com/module.zip" URL 경로로 관리 에어갭 환경
# Terraform Registry — 버전 제약 필수
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"   # 5.x 최신 (6.0 미만)
  # ...
}

# GitHub — ref로 태그/브랜치/커밋 고정
module "vpc" {
  source = "github.com/myorg/terraform-modules//modules/vpc?ref=v2.3.1"
  # ...
}

# 로컬 경로 — 버전 인수 사용 불가
module "vpc" {
  source = "./modules/vpc"
  # version은 사용할 수 없음 — 항상 현재 파일 참조
  # ...
}

주의: version 인수는 Terraform Registry 소스에서만 사용 가능합니다. Git 소스는 ?ref=로, 로컬 경로는 버전 관리 자체가 불가능합니다.

Input Variables 설계: 모듈의 공개 인터페이스

모듈의 variables.tf는 함수의 파라미터와 같습니다. 잘 설계된 input은 모듈의 유연성과 재사용성을 결정합니다.

# modules/vpc/variables.tf

variable "name" {
  description = "VPC 이름 태그에 사용되는 프로젝트명"
  type        = string

  validation {
    condition     = length(var.name) > 0 && length(var.name) = 2
    error_message = "고가용성을 위해 최소 2개 AZ가 필요합니다."
  }
}

variable "enable_nat_gateway" {
  description = "NAT Gateway 생성 여부"
  type        = bool
  default     = true
}

variable "tags" {
  description = "모든 리소스에 부착될 공통 태그"
  type        = map(string)
  default     = {}
}

Input 설계 원칙

  • 필수 vs 선택: default가 없으면 필수, 있으면 선택입니다. 합리적인 기본값이 있는 항목에만 default를 설정합니다.
  • validation 블록: Terraform 0.13+에서 지원합니다. plan 단계에서 잘못된 입력을 조기에 잡아줍니다.
  • description 필수: Registry에 게시하면 자동으로 문서화됩니다. 로컬 모듈에서도 terraform-docs로 README를 자동 생성할 수 있습니다.
  • sensitive 마킹: 비밀번호·API 키는 sensitive = true를 설정하여 plan/apply 출력에서 마스킹합니다.

Output Values 설계: 모듈 간 데이터 전달

# modules/vpc/outputs.tf

output "vpc_id" {
  description = "생성된 VPC의 ID"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "프라이빗 서브넷 ID 목록"
  value       = aws_subnet.private[*].id
}

output "public_subnet_ids" {
  description = "퍼블릭 서브넷 ID 목록"
  value       = aws_subnet.public[*].id
}

output "nat_gateway_ips" {
  description = "NAT Gateway의 Elastic IP 주소"
  value       = var.enable_nat_gateway ? aws_eip.nat[*].public_ip : []
}
# root module에서 child module의 output 참조
module "vpc" {
  source = "./modules/vpc"
  # ...
}

module "eks" {
  source = "./modules/eks"
  
  vpc_id     = module.vpc.vpc_id              # output 참조
  subnet_ids = module.vpc.private_subnet_ids  # output 참조
}

# root module의 output으로 노출 (다른 프로젝트에서 remote_state로 참조 가능)
output "vpc_id" {
  value = module.vpc.vpc_id
}

설계 원칙: output은 “다른 모듈이 필요로 할 수 있는 값”만 노출합니다. 리소스의 모든 속성을 노출하는 것은 캡슐화를 깨뜨립니다. 변경 시 모든 소비자에 영향을 미치므로, output은 모듈의 공개 API로 취급하고 신중하게 관리해야 합니다.

모듈 버전 관리: 시맨틱 버저닝과 version 제약

version 제약 의미 사용 시점
= 1.2.3 정확히 1.2.3 재현성이 최우선 (프로덕션 고정)
~> 1.2 ≥ 1.2.0, < 2.0.0 마이너·패치 자동 업데이트 허용
~> 1.2.0 ≥ 1.2.0, < 1.3.0 패치만 자동 업데이트
>= 1.0, < 2.0 복합 조건 세밀한 범위 지정
# 프로덕션 권장: 패치만 자동 업데이트
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.8.0"   # 5.8.x만 허용
}

# 개발 환경: 마이너 업데이트 허용
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"     # 5.x 전체 허용
}

운영 규칙: terraform init.terraform.lock.hcl에 설치된 모듈의 정확한 버전을 기록합니다. 이 lock 파일을 반드시 Git에 커밋하여 팀원 간 동일한 버전이 사용되도록 합니다.

모듈 구성 패턴: Flat vs Nested vs Facade

패턴 1: Flat Modules (권장 시작점)

infrastructure/
├── main.tf              # root module
├── modules/
│   ├── vpc/             # 독립 모듈
│   ├── eks/             # 독립 모듈
│   ├── rds/             # 독립 모듈
│   └── redis/           # 독립 모듈
└── environments/
    ├── dev.tfvars
    ├── staging.tfvars
    └── prod.tfvars

root module에서 각 child module을 직접 호출하고, 모듈 간 의존성은 output → input으로 연결합니다.

패턴 2: Nested Modules (중형 이상 프로젝트)

modules/
└── eks-cluster/           # 상위 모듈
    ├── main.tf            # 하위 모듈 호출
    ├── modules/
    │   ├── node-group/    # 하위 모듈
    │   └── addons/        # 하위 모듈
    ├── variables.tf
    └── outputs.tf

상위 모듈이 하위 모듈들을 조합하여 하나의 논리 단위를 구성합니다. 이 패턴에서 주의할 점은 중첩 깊이를 2단계 이하로 유지하는 것입니다. 3단계 이상은 디버깅이 극도로 어려워집니다.

패턴 3: Facade Module (환경별 구성 캡슐화)

# environments/prod/main.tf — Facade 역할
module "platform" {
  source = "../../modules/platform"
  
  environment        = "prod"
  vpc_cidr           = "10.0.0.0/16"
  eks_node_count     = 5
  rds_instance_class = "db.r6g.xlarge"
  enable_monitoring  = true
}

# modules/platform/main.tf — 내부에서 여러 모듈 조합
module "vpc" {
  source     = "../vpc"
  cidr_block = var.vpc_cidr
  # ...
}

module "eks" {
  source     = "../eks"
  vpc_id     = module.vpc.vpc_id
  node_count = var.eks_node_count
  # ...
}

module "rds" {
  source         = "../rds"
  subnet_ids     = module.vpc.private_subnet_ids
  instance_class = var.rds_instance_class
  # ...
}

패턴 비교

패턴 장점 단점 적합한 규모
Flat 단순, 의존성 투명 root module 비대화 소규모 (모듈 5개 이하)
Nested 관련 리소스 응집, 재사용 단위 확대 깊은 중첩 시 디버깅 난이도 증가 중규모
Facade 환경별 차이를 변수 하나로 제어 추상화 층이 많아 plan 결과 추적 어려움 대규모·멀티 환경

모듈에서 Provider 설정: 전달 패턴과 함정

Terraform의 공식 권장 사항은 child module이 자체적으로 provider를 설정하지 않고, root module에서 전달받는 것입니다.

# root module: provider 설정
provider "aws" {
  region = "ap-northeast-2"
  alias  = "seoul"
}

provider "aws" {
  region = "us-east-1"
  alias  = "virginia"
}

# child module에 provider 전달
module "cdn" {
  source = "./modules/cloudfront"

  providers = {
    aws = aws.virginia    # CloudFront + ACM은 us-east-1 필수
  }
}
# modules/cloudfront/versions.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
      # configuration_aliases는 쓰지 않음 — 기본 aws provider만 수신
    }
  }
}

함정: child module 내부에서 provider 블록을 직접 정의하면, 해당 모듈을 countfor_each로 호출할 수 없습니다. Terraform은 “provider를 자체 설정하는 모듈은 반복 호출이 불가능하다”는 제약을 명시하고 있습니다.

for_each로 모듈 반복 호출

Terraform 0.13+에서 for_each를 모듈 블록에 사용할 수 있습니다. 동일한 모듈을 여러 설정으로 반복 생성할 때 유용합니다.

locals {
  databases = {
    users = {
      instance_class = "db.t3.medium"
      storage_gb     = 50
      multi_az       = false
    }
    orders = {
      instance_class = "db.r6g.large"
      storage_gb     = 200
      multi_az       = true
    }
    analytics = {
      instance_class = "db.r6g.xlarge"
      storage_gb     = 500
      multi_az       = true
    }
  }
}

module "rds" {
  source   = "./modules/rds"
  for_each = local.databases

  name           = each.key
  instance_class = each.value.instance_class
  storage_gb     = each.value.storage_gb
  multi_az       = each.value.multi_az
  subnet_ids     = module.vpc.private_subnet_ids
}

# 특정 인스턴스의 output 접근
output "orders_db_endpoint" {
  value = module.rds["orders"].endpoint
}

# 전체 인스턴스의 endpoint 맵
output "all_db_endpoints" {
  value = { for k, v in module.rds : k => v.endpoint }
}

주의: for_each의 키는 state에서 리소스 주소로 사용됩니다(module.rds["orders"]). 키를 변경하면 Terraform은 기존 리소스를 삭제하고 새로 생성합니다. moved 블록으로 마이그레이션하세요.

depends_on: 모듈 간 암묵적 의존성 명시

대부분의 모듈 간 의존성은 output → input 참조로 자동 해결됩니다. 하지만 데이터 흐름으로 표현할 수 없는 의존성이 있을 때 depends_on을 사용합니다.

# IAM 정책이 적용된 후에 EKS를 생성해야 하지만,
# IAM ARN을 EKS 모듈에 전달하는 것으로는 의존성이 부족한 경우
module "iam" {
  source = "./modules/iam"
}

module "eks" {
  source = "./modules/eks"
  
  role_arn = module.iam.eks_role_arn   # 데이터 의존성
  
  depends_on = [module.iam]           # 전체 모듈 완료 후 실행 보장
}

주의: depends_on을 모듈에 사용하면, 해당 모듈의 모든 리소스가 depends_on 대상 이후로 지연됩니다. 이는 plan/apply 시간을 크게 증가시킬 수 있으므로, 가능하면 output → input 참조로 해결하고 depends_on은 최후의 수단으로 사용합니다.

모듈 리팩터링: moved 블록으로 state 안전하게 이동

모듈 구조를 변경할 때 가장 위험한 것은 state에서 리소스가 “삭제 + 재생성”으로 처리되는 것입니다. Terraform 1.1+의 moved 블록으로 이를 방지합니다.

# 리소스를 모듈 안으로 이동
moved {
  from = aws_vpc.main
  to   = module.vpc.aws_vpc.main
}

# 모듈 이름 변경
moved {
  from = module.old_name
  to   = module.new_name
}

# for_each 키 변경
moved {
  from = module.rds["user-db"]
  to   = module.rds["users"]
}

moved 블록은 terraform plan에서 “~ moved”로 표시되며, 실제 인프라 변경 없이 state만 업데이트됩니다. 마이그레이션이 완료되면 moved 블록을 제거해도 됩니다.

모듈 설계 Anti-Pattern 5가지

  1. “God Module”: VPC + EKS + RDS + Redis를 하나의 모듈에 넣으면 변경 영향 범위가 너무 넓어집니다. 모듈 하나가 10개 이상의 리소스를 포함하면 분할을 검토하세요.
  2. 모듈 안에서 provider 설정: for_each/count 사용 불가, 테스트 어려움. provider는 반드시 root에서 전달합니다.
  3. output 없는 모듈: 다른 모듈과 통합할 수 없어 재사용이 불가능해집니다. 최소한 생성된 리소스의 ID와 ARN은 output으로 노출하세요.
  4. version 미지정: Registry 모듈을 version 없이 사용하면 terraform init -upgrade 시 예고 없이 최신 버전으로 올라갑니다. 프로덕션에서는 반드시 version을 고정합니다.
  5. 깊은 중첩 (3단계 이상): module.a.module.b.module.c 형태는 plan 출력을 읽기 어렵게 만들고, state 구조가 복잡해져 디버깅이 극도로 어렵습니다.

모듈 설계 체크리스트

항목 확인 사항
경계 하나의 논리적 인프라 단위를 표현하는가?
variables.tf description·type·validation이 모두 있는가?
outputs.tf 소비자 모듈이 필요로 하는 값을 노출하는가?
versions.tf required_providers와 terraform 버전 제약이 있는가?
Provider child module이 자체 provider를 설정하지 않는가?
버전 관리 Registry 모듈에 version, Git에 ?ref가 고정되어 있는가?
중첩 깊이 2단계 이하로 유지되는가?

참고 자료

📥 관련 무료 이북

쿠버네티스 입문 가이드 — 실전 가이드 무료 제공

무료로 받기 →

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux