Terraform 모듈은 인프라 코드를 재사용하고 관리하는 데 있어 핵심적인 도구입니다. 하지만 모듈을 단순히 코드를 묶어두는 폴더로만 생각해서는 안 됩니다. 진정으로 유연하고 확장 가능한 모듈을 설계하려면 몇 가지 중요한 원칙과 주의사항을 이해해야 합니다.
1. Terraform 모듈의 기본 개념
Terraform 모듈은 폴더에 있는 구성 파일(.tf 파일)의 묶음입니다. 지금까지 우리가 작성했던 모든 Terraform 구성도 사실상 하나의 모듈이었지만, 직접 배포를 수행했기 때문에 "루트 모듈(Root Module)"이라고 불립니다.
모듈 생성의 첫걸음
기존에 작성된 인프라 코드를 재사용 가능한 모듈로 변환하는 것은 매우 간단합니다.
- 기존 리소스 회수: 먼저 terraform destroy 명령어를 통해 기존에 배포된 리소스들을 삭제합니다.
- 파일 이동: 재사용할 리소스 정의 파일들(예: main.tf, variables.tf, outputs.tf, user-data.sh)을 새로운 모듈 폴더(예: modules/services/webserver-cluster)로 이동시킵니다.
- provider 블록 제거: 모듈 내부에 provider 블록을 정의하지 않습니다. provider는 모듈을 호출하는 루트 모듈에서 직접 정의해야 합니다.
모듈 사용 방법
모듈을 사용하려면 루트 모듈의 .tf 파일에 module 블록을 추가합니다.
# 예시: 루트 모듈 (stage/services/webserver-cluster/main.tf)
provider "aws" { # provider는 모듈 내부가 아닌 루트에서 정의
region = "us-east-1"
}
module "webserver_cluster" { # 모듈의 논리적인 이름
source = "../../modules/services/webserver-cluster" # 모듈 코드의 상대 경로
# 여기에 모듈의 입력 변수(아래 2.1에서 설명)를 전달
}
이렇게 하면 동일한 모듈을 복사-붙여넣기 없이 여러 환경(예: 스테이징, 상용)에서 재사용할 수 있습니다. 모듈을 추가하거나 source 매개변수를 수정할 때마다 terraform init 명령을 실행하여 변경사항을 동기화해야 합니다
2. 모듈의 유연성을 위한 입력 및 출력
모듈을 재사용 가능하게 만들려면, 하드코딩된 값 대신 **구성 가능한 입력(Input Variables)**을 받고, 배포된 리소스의 정보를 **출력(Outputs)**해야 합니다.
2.1. 모듈 입력: variable
일반적인 프로그래밍 언어의 함수가 입력 매개변수를 받는 것처럼, Terraform 모듈도 variable 블록을 통해 입력 변수를 정의할 수 있습니다.
# 예시: modules/services/webserver-cluster/variables.tf
variable "cluster_name" {
description = "The name to use for all the cluster resources"
type = string
}
variable "db_remote_state_bucket" {
description = "The name of the S3 bucket for the database’s remote state"
type = string
}
# ... (instance_type, min_size, max_size 등 다른 변수들)
모듈 내부에서는 이 변수들을 ${var.변수이름} 형태로 참조합니다 (예: name = "${var.cluster_name}-elb").
모듈을 호출할 때는 이 변수들에 값을 전달합니다.
# 예시: 루트 모듈에서 모듈 호출 시 변수 전달
module "webserver_cluster" {
source = "../../modules/services/webserver-cluster"
cluster_name = "webservers-stage" # 스테이징 환경 이름
db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
db_remote_state_key = "stage/data-stores/mysql/terraform.tfstate"
instance_type = "t2.micro" # 인스턴스 타입
min_size = 2 # 최소 인스턴스 수
max_size = 2 # 최대 인스턴스 수
}
이렇게 하면 동일한 모듈 코드를 사용하면서도, 환경(스테이징/상용)에 따라 다른 이름, 다른 DB 상태 파일 경로, 다른 VM 사양 등을 유연하게 설정할 수 있습니다.
2.2. 모듈 출력: output
모듈이 배포한 리소스의 특정 정보(예: ASG 이름, ELB DNS 이름)를 다른 Terraform 구성에서 사용하거나, terraform output 명령으로 확인하려면 output 블록을 사용합니다.
# 예시: modules/services/webserver-cluster/outputs.tf
output "asg_name" {
description = "The name of the Auto Scaling Group"
value = aws_autoscaling_group.example.name
}
output "elb_dns_name" {
description = "The DNS name of the Elastic Load Balancer"
value = aws_elb.example.dns_name
}
루트 모듈에서 이 출력 값들을 참조할 때는 "${module.모듈_이름.출력_변수_이름}" 구문을 사용합니다.
# 예시: 루트 모듈에서 모듈 출력 참조
resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
# ...
autoscaling_group_name = module.webserver_cluster.asg_name
# ...
}
output "elb_dns_name_from_module" {
value = module.webserver_cluster.elb_dns_name
}
이렇게 모듈 출력을 사용하면, 모듈 외부의 리소스가 모듈 내부에서 생성된 리소스의 동적인 속성(예: ASG의 실제 이름)을 알 수 있게 되어 인프라 간의 의존성을 효과적으로 관리할 수 있습니다.
3. 모듈 생성 시 주의할 점 (핵심 모범 사례)
모듈의 유연성과 안정성을 보장하기 위해 반드시 지켜야 할 두 가지 중요한 주의사항이 있습니다.
3.1. 파일 경로 처리: path.module의 중요성
- 문제점: 상대 경로의 함정 file("user-data.sh")와 같이 상대 경로를 지정하면, Terraform은 terraform apply 명령이 실행된 현재 작업 디렉터리를 기준으로 파일을 찾습니다. 이는 하위 모듈에서 사용될 때 파일을 찾지 못하는 오류를 유발합니다.
- 해결책: path.module 사용 ("${path.module}/파일이름") path.module은 현재 Terraform 코드가 작성된 모듈의 파일 시스템 경로를 나타내는 특별한 내장 변수입니다. template = "${file("${path.module}/user-data.sh")}"와 같이 사용하면, file() 함수는 항상 해당 모듈이 위치한 디렉터리를 기준으로 파일을 찾게 됩니다. 이는 모듈의 이식성을 극대화합니다.
3.2. 인라인 블록 vs. 별도의 리소스: 유연성을 위한 선택
Terraform의 일부 리소스는 관련된 설정을 인라인 블록으로 정의할 수도 있고, 별도의 리소스로 정의할 수도 있습니다.
- 문제점: 인라인 블록과 별도 리소스의 혼합 사용 금지 Terraform은 인라인 블록과 별도 리소스를 동시에 사용하여 동일한 규칙을 정의하는 것을 허용하지 않습니다. 이렇게 하면 라우팅 규칙이 충돌하거나 서로 덮어쓰는 오류가 발생합니다. 둘 중 하나만 선택해야 합니다.
- 모듈 생성 시 권장 사항: 항상 별도의 리소스 사용 모듈을 만들 때는 항상 인라인 블록 대신 별도의 리소스를 사용하는 것이 좋습니다. 이유 (유연성): 별도의 리소스를 사용하면 모듈의 유연성과 확장성이 크게 향상됩니다. 예를 들어, 모듈이 기본적인 보안 그룹을 생성하고 SSH/HTTP 규칙을 별도의 aws_security_group_rule 리소스로 정의했다면, 이 모듈을 호출하는 루트 모듈에서 모듈 외부에 추가적인 aws_security_group_rule 리소스를 정의하여 특정 포트를 추가로 열 수 있습니다. 인라인 블록 방식으로는 이러한 유연한 확장이 어렵습니다.
- 영향을 받는 다른 Terraform 리소스: 이러한 인라인/별도 리소스 제약 사항은 aws_security_group 외에도 다음과 같은 여러 Terraform 리소스에 영향을 줍니다.
- aws_route_table과 aws_route (라우팅 규칙)
- aws_network_acl과 aws_network_acl_rule (네트워크 ACL 규칙)
- aws_elb와 aws_elb_attachment (로드 밸런서와 인스턴스 연결)
4. 간단한 모듈 실습
현재 프로젝트의 디렉터리 구조는 다음과 같습니다.
cheol@MZC01-CLOUDLSC0406:/mnt/c/code/terraform_code/module_project$ tree
.
├── main.tf
├── modules
│ ├── rg
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vm
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── variables.tf
4 directories, 8 files
1. main.tf
- module "rg_module" {} 블록:
- 이 부분이 modules/rg/ 하위 모듈을 호출하는 부분입니다.
- rg_module: 이 모듈 호출에 부여한 논리적인 이름입니다. 루트 모듈 내에서 이 이름을 통해 하위 모듈을 참조합니다.
- source = "./modules/rg": 호출할 하위 모듈의 위치를 지정합니다. .은 현재 디렉터리(module_project/)를 의미하므로, module_project/modules/rg 경로에 있는 모듈을 사용하겠다는 뜻입니다.
- resource_group_name = var.rg_name: 루트 모듈의 입력 변수(var.rg_name)의 값을 하위 모듈의 입력 변수(resource_group_name)로 전달합니다. location 변수도 동일하게 전달됩니다.
- output "final_rg_id" {} 및 output "final_rg_name" {} 블록:
- 이 부분은 하위 모듈(modules/rg/)이 생성한 리소스 그룹의 정보를 루트 모듈의 출력 값으로 다시 공개하는 역할을 합니다.
- value = module.rg_module.created_resource_group_id: rg_module이라는 논리적 이름으로 호출된 하위 모듈의 created_resource_group_id라는 출력 변수의 값을 가져와 final_rg_id라는 루트 모듈의 출력 값으로 설정합니다. created_resource_group_name도 동일하게 처리됩니다.
# Terraform 설정
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
}
# 'simple-resource-group' 하위 모듈 호출
# source는 루트 모듈 디렉터리(simple-root-project/)를 기준으로 한 상대 경로입니다.
module "rg_module" {
source = "./modules/rg" # 모듈의 상대 경로
# 모듈의 입력 변수에 값 전달
resource_group_name = var.rg_name
location = var.rg_location
}
# 모듈의 출력 값을 루트 모듈의 출력으로 다시 공개
output "final_rg_id" {
description = "모듈을 통해 생성된 리소스 그룹의 최종 ID"
value = module.rg_module.created_resource_group_id
}
output "final_rg_name" {
description = "모듈을 통해 생성된 리소스 그룹의 최종 이름"
value = module.rg_module.created_resource_group_name
}
2. variables.tf
# 루트 모듈의 입력 변수들을 정의합니다.
variable "rg_name" {
description = "생성할 리소스 그룹의 이름 (모듈에 전달)"
type = string
default = "terzmy-rg"
}
variable "rg_location" {
description = "리소스 그룹이 배포될 Azure 지역 (모듈에 전달)"
type = string
default = "koreacentral"
}
3. ./modules/rg/main.tf
# Azure 리소스 그룹을 생성합니다.
resource "azurerm_resource_group" "terzmy_rg" {
name = var.resource_group_name
location = var.location
tags = {
ManagedBy = "TerraformModule"
Purpose = "SimpleDemo"
}
}
4. ./modules/rg/outputs.tf
- output "created_resource_group_id" {}:
- created_resource_group_id: 이 모듈이 외부에 제공할 출력 변수의 이름입니다.
- value = azurerm_resource_group.terzmy_rg.id: azurerm_resource_group.terzmy_rg라는 리소스 그룹이 Azure에 성공적으로 생성된 후, Azure로부터 자동으로 부여받은 고유 ID 값을 이 출력 변수에 할당합니다.
- output "created_resource_group_name" {}:
- created_resource_group_name: 이 모듈이 외부에 제공할 또 다른 출력 변수의 이름입니다.
- value = azurerm_resource_group.terzmy_rg.name: azurerm_resource_group.terzmy_rg 리소스의 이름 속성 값을 이 출력 변수에 할당합니다.
# 모듈의 출력 값들을 정의합니다.
output "created_resource_group_id" {
description = "생성된 리소스 그룹의 ID"
value = azurerm_resource_group.terzmy_rg.id
}
output "created_resource_group_name" {
description = "생성된 리소스 그룹의 이름"
value = azurerm_resource_group.terzmy_rg.name
}
5. ./modules/rg/variables.tf
이 파일은 modules/rg/main.tf에서 사용될 입력 변수들을 정의합니다.
- variable "resource_group_name" {}:
- modules/rg/main.tf의 resource "azurerm_resource_group" "terzmy_rg" 블록에서 name = var.resource_group_name으로 사용되는 변수입니다.
- 이 변수는 루트 모듈에서 module "rg_module"을 호출할 때 resource_group_name 인자로 값을 전달받습니다.
- variable "location" {}:
- modules/rg/main.tf의 resource "azurerm_resource_group" "terzmy_rg" 블록에서 location = var.location으로 사용되는 변수입니다.
- 이 변수도 루트 모듈에서 location 인자로 값을 전달받습니다.
# 모듈의 입력 변수들을 정의합니다.
variable "resource_group_name" {
description = "생성할 리소스 그룹의 이름"
type = string
}
variable "location" {
description = "리소스 그룹이 생성될 Azure 지역"
type = string
}
terraform 초기화 및 실행 , 배포 진행 합니다.
terraform init
terraform plan
terraform apply --auto-approve

코드 간의 연관성 요약
- **루트 모듈 (module_project/main.tf)**은 module "rg_module" 블록을 통해 **하위 모듈 (modules/rg/)**을 호출합니다.
- 루트 모듈의 variables.tf에 정의된 var.rg_name과 var.rg_location 값이 module "rg_module" 블록의 인자를 통해 하위 모듈의 modules/rg/variables.tf에 정의된 resource_group_name과 location 변수로 전달됩니다.
- 하위 모듈의 modules/rg/main.tf는 전달받은 변수 값을 사용하여 azurerm_resource_group "terzmy_rg" 리소스를 실제로 Azure에 생성합니다.
- 하위 모듈의 modules/rg/outputs.tf는 생성된 azurerm_resource_group.terzmy_rg 리소스의 id와 name 속성을 created_resource_group_id와 created_resource_group_name이라는 출력 변수로 공개합니다.
- 루트 모듈의 module "rg_module" 블록은 하위 모듈이 공개한 출력 값들을 module.rg_module.created_resource_group_id와 module.rg_module.created_resource_group_name 형태로 참조하여, 이를 다시 루트 모듈의 output으로 외부에 공개합니다.
이러한 방식으로 Terraform 모듈은 코드의 계층화를 통해 복잡한 인프라를 체계적으로 관리하고 재사용성을 높입니다.
'Terraform' 카테고리의 다른 글
| [Terraform] 복잡한 조건문 (조건 분기) (4) | 2025.08.08 |
|---|---|
| [Terraform] 반복문 및 조건문 (2) | 2025.08.05 |
| [Terraform] 모듈 버전 관리 with Github (2) | 2025.08.04 |
| [Terraform] 상태 파일(tfstate) 관리 (0) | 2025.07.31 |
| [Terraform] 동적 데이터 처리와 tfstate 상태 관리 분석 with Azure Blob (2) | 2025.07.31 |