클라우드 인프라를 코드로 관리하는 IaC(Infrastructure as Code) 시대에 Terraform은 필수적인 도구로 자리 잡았습니다. 하지만 실제 운영 환경의 인프라는 단순히 정적인 리소스들의 집합이 아닙니다. 데이터베이스 주소, VM의 IP, 서비스 엔드포인트 등 동적으로 결정되는 정보를 다른 리소스나 애플리케이션에 전달해야 하는 경우가 빈번합니다.
또한, 여러 팀이 각자의 인프라 스택을 관리할 때, 서로의 출력 값을 참조해야 하는 프로젝트 간의 데이터 공유도 중요합니다. 이 모든 과정에서 Terraform 상태 파일(tfstate)을 안전하고 효율적으로 관리하는 것은 DevOps 엔지니어의 핵심 역량입니다.
이번 블로그 포스팅에서는 Terraform의 terraform_remote_state, file(), templatefile(), base64encode()와 같은 동적 데이터 처리 기능들을 활용하여, 서로 의존하는 두 개의 인프라 스택을 배포하고 관리하는 과정을 상세히 다룹니다. 이를 통해 Terraform의 강력함과 상태 관리의 중요성을 함께 이해할 수 있을 것입니다.
1. 전체적인 프로세스 흐름
이번 실습에서는 두 개의 독립적인 Terraform 프로젝트를 사용하여, 서로의 정보를 참조하며 인프라를 배포합니다.
- db-project:
- 역할: 가상의 데이터베이스 서버 정보를 정의하고, 그 주소와 포트 번호를 Terraform output으로 외부에 노출합니다. (실제 데이터베이스 인스턴스를 생성하지는 않습니다.)
- 상태 관리: 이 프로젝트의 tfstate 파일은 Azure Blob Storage의 특정 컨테이너(tfstate-db)에 저장됩니다.
- web-project:
- 역할: Azure VM을 웹 서버로 배포하고, 이 VM의 초기 설정 스크립트(user data)에 db-project에서 출력한 가상의 데이터베이스 주소와 포트 번호를 동적으로 주입합니다.
- 상태 관리: 이 프로젝트의 tfstate 파일은 Azure Blob Storage의 다른 컨테이너(tfstate-web)에 저장됩니다.
프로세스 흐름 요약:
- db-project가 먼저 배포되어 가상 DB 정보를 Azure Blob Storage의 상태 파일에 기록합니다.
- web-project는 db-project의 상태 파일을 읽기 전용으로 참조하여 가상 DB 정보를 가져옵니다.
- 가져온 정보를 바탕으로 web-project는 웹 서버 VM을 배포하고, VM의 시작 스크립트에 이 정보를 주입하여 웹 서버가 동적으로 DB 정보를 알 수 있도록 합니다.
2. 내부 프로세스 아키텍처
web-project 내부에서 db-project의 데이터가 웹 서버 VM의 user data로 주입되는 과정은 다음과 같습니다.
1. db-project 배포 및 출력:
- db-project/main.tf에서 output "db_address"와 output "db_port"를 정의하여 가상 DB 정보를 외부에 공개합니다.
- terraform apply 후, 이 출력 값들은 db-project.tfstate 파일에 기록되어 Azure Blob Storage (tfstate-db 컨테이너)에 저장됩니다.


2. web-project에서 원격 상태 참조:
- web-project/main.tf에서 data "terraform_remote_state" "db" 데이터 소스를 정의합니다.
- 이 데이터 소스는 db-project의 tfstate 파일이 저장된 Azure Blob Storage의 위치를 config 블록에 명시하여, 해당 상태 파일의 output 값들을 읽어옵니다. (예: data.terraform_remote_state.db.outputs.db_address)

3. user-data 스크립트 템플릿:
- web-project/user-data.sh.tpl 파일은 웹 서버 VM이 부팅될 때 실행될 셸 스크립트 템플릿입니다.
- 이 템플릿 파일 내부에는 ${db_address}, ${db_port}와 같은 플레이스홀더(placeholder)가 포함되어 있습니다.

4. templatefile() 함수를 통한 동적 주입:
- web-project/main.tf의 azurerm_linux_virtual_machine 리소스 내 user_data 속성에서 templatefile() 함수를 사용합니다.
- templatefile("${path.module}/user-data.sh.tpl", {...})는 user-data.sh.tpl 파일을 읽고, vars 인자로 전달된 값들(예: data.terraform_remote_state.db.outputs.db_address)을 템플릿 내부의 플레이스홀더에 주입하여 최종 셸 스크립트 문자열을 생성합니다.
5. base64encode() 함수를 통한 인코딩:
- Azure VM의 user_data 속성은 Base64 인코딩된 문자열을 요구하므로, base64encode() 함수로 templatefile()의 결과를 다시 인코딩합니다.

6. VM에 user_data 전달 및 실행:
- Terraform은 Base64 인코딩된 user_data를 Azure API를 통해 VM에 전달합니다.
- VM 내부의 cloud-init 서비스가 VM 부팅 시 이 스크립트를 자동으로 디코딩하여 실행합니다.
7. 상세 다이어그램: db-project (Outputs) -> Azure Blob (db-project.tfstate) -> web-server-project (data.terraform_remote_state.db) -> templatefile() -> base64encode() -> Azure VM (user_data) -> cloud-init -> Apache2 설치 및 index.html 생성
3. 핵심 기능 설명
1. data.terraform_remote_state (원격 상태 데이터 소스)
- 역할: 다른 Terraform 구성(프로젝트)에 의해 관리되는 상태 파일(tfstate)의 출력 값들을 현재 Terraform 구성에서 읽기 전용으로 가져올 수 있도록 해주는 데이터 소스입니다.
2. file() 함수
- 역할: Terraform이 실행되는 로컬 파일 시스템에서 지정된 파일의 내용을 읽어와 문자열로 반환하는 내장 함수입니다.
3. templatefile() 함수
- 역할: 템플릿 파일(user-data.sh.tpl)에 Terraform 변수나 다른 데이터 소스(예: data.terraform_remote_state)에서 가져온 **동적인 값들을 주입하여 최종 문자열을 렌더링(변환)**하는 내장 함수입니다.
4. base64encode() 함수
- 역할: 주어진 문자열을 Base64 형식으로 인코딩하는 내장 함수입니다.
4. 실습 준비 : Azure 백엔드 인프라 구축
Terraform 프로젝트의 상태 파일을 저장하고, 프로젝트 간에 공유할 수 있도록 Azure Blob Storage 인프라를 미리 구축해야 합니다.
핵심: 상태 파일용 리소스 그룹, 스토리지 계정, 컨테이너는 Terraform 프로젝트에 의해 직접 관리되거나 삭제되지 않도록 별도로 생성하고 관리하는 것이 중요합니다. 본 실습에서는 Azure Blob Storage에서 관리되도록 하였습니다.
Terraform 상태 파일은 백엔드 인프라를 통해서 생성 및 관리됩니다.
상태 파일에는 해당 프로젝트의 리소스에 관한 모든 정보가 저장되어있습니다.
그러므로, terraform plan, apply, destroy 명령어를 진행할때 Terraform은 상태 파일을 확인하여 명령어를 실행합니다.
# --- 단일 백엔드 인프라 생성 ---
# 공용 변수
$LOCATION="koreacentral"
# tfstate 리소스 그룹 이름
$SHARED_STATE_RG="terzmy-tfstate-rg"
# tfstate 스토리지 계정 이름
$SHARED_STATE_SA="terzmytfstatestg"
# 컨테이너 이름 (db-project용)
$DB_STATE_CONTAINER="tfstate-db"
# 컨테이너 이름 (web-server-project용)
$WEB_STATE_CONTAINER="tfstate-web"
# 리소스 그룹 및 스토리지 계정 배포
az group create --name $SHARED_STATE_RG --location $LOCATION
az storage account create --name $SHARED_STATE_SA --resource-group $SHARED_STATE_RG --location $LOCATION --sku Standard_LRS --kind StorageV2
# 각 프로젝트별 Blob 컨테이너 생성
az storage container create --name $DB_STATE_CONTAINER --account-name $SHARED_STATE_SA --resource-group $SHARED_STATE_RG
az storage container create --name $WEB_STATE_CONTAINER --account-name $SHARED_STATE_SA --resource-group $SHARED_STATE_RG
echo "--- 중요: Azure Portal에서 Blob 버전 관리 활성화 ---"
echo "다음 스토리지 계정으로 이동하여 '데이터 보호' -> 'Blob 버전 관리 사용'을 '켜기'로 설정하세요."
echo "1. $SHARED_STATE_SA"
5. 실습 과정 : 단계별 Terraform 적용
실습 과정에서는 WSL 환경에서 진행하였습니다.
전체적인 프로젝트 디렉터리 구조는 다음과 같습니다. (모듈화는 진행하지 않았습니다.)
cheol@MZC01-CLOUDLSC0406:/mnt/c/code/terraform_code/dynamic-data-processing$ tree
.
├── create_web_db.txt
├── db-project
│ ├── main.tf
│ └── variables.tf
└── web-project
├── main.tf
├── user-data.sh.tpl
└── variables.tf
1. db-project 배포
- main.tf
# db-project/main.tf
# Terraform 설정
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
# 백엔드 설정: db-project의 상태 파일을 저장할 Blob Storage 정보
# TODO: 1단계에서 생성한 'SHARED_STATE_RG', 'SHARED_STATE_SA', 'DB_STATE_CONTAINER' 값들로 직접 변경하세요.
backend "azurerm" {
resource_group_name = "terzmy-tfstate-rg" # SHARED_STATE_RG 값으로 변경
storage_account_name = "terzmytfstatestg" # SHARED_STATE_SA 값으로 변경
container_name = "tfstate-db" # DB_STATE_CONTAINER 값으로 변경
key = "db-project.tfstate" # 이 프로젝트의 상태 파일 이름
}
}
provider "azurerm" {
features {}
}
# 가상 데이터베이스를 나타내는 리소스 그룹 생성 (실제 DB 아님, 단지 예시)
resource "azurerm_resource_group" "db_rg" {
name = var.db_resource_group_name
location = var.location
tags = {
Environment = var.environment
Service = "Database"
}
}
# 데이터베이스 주소와 포트 번호를 출력 (이 값이 web-project로 전달됩니다)
output "db_address" {
description = "가상 데이터베이스의 주소"
value = "db-server.terzmy.com" # 실제 DB 주소라고 가정
}
output "db_port" {
description = "가상 데이터베이스의 포트"
value = 5432 # 실제 DB 포트라고 가정 (예: PostgreSQL)
}
- variables.tf
# db-project/variables.tf
variable "db_resource_group_name" {
description = "가상 데이터베이스 리소스 그룹의 이름"
type = string
default = "terzmy-db-rg"
}
variable "location" {
description = "리소스가 배포될 Azure 지역"
type = string
default = "koreacentral"
}
variable "environment" {
description = "배포 환경"
type = string
default = "dev"
}
main.tf 와 variables.tf 파일을 구성하고나서 테라폼 초기화 및 배포를 진행합니다.
# terraform 초기화
terraform init
# terraform 계획 확인
terraform plan
# terraform 적용 및 배포
terraform apply --auto-approve
2. web-project 배포
- main.tf
# web-server-project/main.tf
# Terraform 설정
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
# 백엔드 설정: web-server-project의 상태 파일을 저장할 Blob Storage 정보
# TODO: 1단계에서 생성한 'SHARED_STATE_RG', 'SHARED_STATE_SA', 'WEB_STATE_CONTAINER' 값들로 직접 변경하세요.
backend "azurerm" {
resource_group_name = "terzmy-tfstate-rg" # SHARED_STATE_RG 값으로 변경
storage_account_name = "terzmytfstatestg" # SHARED_STATE_SA 값으로 변경
container_name = "tfstate-web" # DB_STATE_CONTAINER 값으로 변경
key = "web-project.tfstate" # 이 프로젝트의 상태 파일 이름
}
}
# AzureRM 프로바이더 설정
provider "azurerm" {
features {}
}
# 'db-project'의 원격 상태 파일에서 출력 값 가져오기
# TODO: 1단계에서 생성한 'SHARED_STATE_RG', 'SHARED_STATE_SA', 'DB_STATE_CONTAINER' 값들로 직접 변경하세요.
data "terraform_remote_state" "db" {
backend = "azurerm"
config = {
resource_group_name = "terzmy-tfstate-rg" # SHARED_STATE_RG 값으로 변경
storage_account_name = "terzmytfstatestg" # SHARED_STATE_SA 값으로 변경
container_name = "tfstate-db" # DB_STATE_CONTAINER 값으로 변경
key = "db-project.tfstate" # 이 프로젝트의 상태 파일 이름
}
}
# 웹 서버 VM을 위한 리소스 그룹 생성
resource "azurerm_resource_group" "web_rg" {
name = var.web_resource_group_name
location = var.location
tags = {
Environment = var.environment
Service = "WebServer"
}
}
# 가상 네트워크 및 서브넷 생성
resource "azurerm_virtual_network" "web_vnet" {
name = "terzmy-web-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.web_rg.location
resource_group_name = azurerm_resource_group.web_rg.name
}
resource "azurerm_subnet" "web_subnet" {
name = "web-subnet"
resource_group_name = azurerm_resource_group.web_rg.name
virtual_network_name = azurerm_virtual_network.web_vnet.name
address_prefixes = ["10.0.1.0/24"]
}
# Public IP 주소 생성
resource "azurerm_public_ip" "web_public_ip" {
name = "web-public-ip"
location = azurerm_resource_group.web_rg.location
resource_group_name = azurerm_resource_group.web_rg.name
allocation_method = "Static"
sku = "Standard"
}
# 네트워크 보안 그룹 (NSG) 생성
resource "azurerm_network_security_group" "web_nsg" {
name = "web-vm-nsg"
location = azurerm_resource_group.web_rg.location
resource_group_name = azurerm_resource_group.web_rg.name
tags = {
Environment = var.environment
Service = "WebServer"
}
}
# NSG 규칙: SSH (22번 포트) 허용
resource "azurerm_network_security_rule" "allow_ssh" {
name = "AllowSSH"
priority = 100 # 우선순위 (낮을수록 높음)
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22" # SSH 기본 포트
source_address_prefix = "*" # 모든 IP에서 허용 (보안을 위해 특정 IP로 제한 권장)
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.web_rg.name
network_security_group_name = azurerm_network_security_group.web_nsg.name
}
# NSG 규칙: HTTP (80번 포트) 허용
resource "azurerm_network_security_rule" "allow_http" {
name = "AllowHTTP"
priority = 110 # SSH보다 낮은 우선순위
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80" # HTTP 기본 포트
source_address_prefix = "*" # 모든 IP에서 허용 (보안을 위해 특정 IP로 제한 권장)
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.web_rg.name
network_security_group_name = azurerm_network_security_group.web_nsg.name
}
# 네트워크 인터페이스 생성
resource "azurerm_network_interface" "web_nic" {
name = "web-nic"
location = azurerm_resource_group.web_rg.location
resource_group_name = azurerm_resource_group.web_rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.web_subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.web_public_ip.id
}
}
# NSG를 NIC에 연결 리소스
resource "azurerm_network_interface_security_group_association" "web_nic_nsg_association" {
network_interface_id = azurerm_network_interface.web_nic.id
network_security_group_id = azurerm_network_security_group.web_nsg.id
}
# Linux 가상 머신 생성
resource "azurerm_linux_virtual_machine" "web_vm" {
name = var.vm_name
resource_group_name = azurerm_resource_group.web_rg.name
location = azurerm_resource_group.web_rg.location
size = var.vm_size
admin_username = var.admin_username
admin_ssh_key {
username = var.admin_username
public_key = file(var.public_ssh_key_path) # SSH 공개 키 파일 경로
}
network_interface_ids = [azurerm_network_interface.web_nic.id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
# user data 스크립트에 동적 데이터 주입 (templatefile 함수 사용)
user_data = base64encode(templatefile("${path.module}/user-data.sh.tpl", {
environment = var.environment
db_address = data.terraform_remote_state.db.outputs.db_address # db-project의 출력값 사용
db_port = data.terraform_remote_state.db.outputs.db_port # db-project의 출력값 사용
server_port = var.web_server_port
}))
tags = {
Environment = var.environment
Project = "WebTest"
}
}
output "web_vm_public_ip" {
description = "웹 서버 VM의 Public IP 주소"
value = azurerm_public_ip.web_public_ip.ip_address
}
output "web_vm_private_ip" {
description = "웹 서버 VM의 Private IP 주소"
value = azurerm_network_interface.web_nic.private_ip_address
}
- variables.tf
가상머신 접속을 위한 ssh 공개 키는 WSL 경로 (예: /mnt/c/code/terraform_code/id_rsa/id_rsa.pub)로 변경합니다.
# web-server-project/variables.tf
variable "web_resource_group_name" {
description = "웹 서버 리소스 그룹의 이름"
type = string
default = "terzmy-web-rg"
}
variable "location" {
description = "리소스가 배포될 Azure 지역"
type = string
default = "koreacentral"
}
variable "environment" {
description = "배포 환경"
type = string
default = "dev"
}
variable "vm_name" {
description = "웹 서버 VM의 이름"
type = string
default = "terzmy-web-vm"
}
variable "vm_size" {
description = "웹 서버 VM의 크기"
type = string
default = "Standard_B1s" # 테스트용 최소 사양 (더 큰 사양 필요 시 변경)
}
variable "admin_username" {
description = "VM 관리자 사용자 이름"
type = string
default = "cheol"
}
variable "public_ssh_key_path" {
description = "VM에 사용할 SSH 공개 키 파일의 로컬 경로 (예: ~/.ssh/id_rsa.pub)"
type = string
# 예: default = "~/.ssh/id_rsa.pub"
# default = "C:/code/terraform_code/id_rsa/id_rsa.pub"
default = "/mnt/c/code/terraform_code/id_rsa/id_rsa.pub"
}
variable "web_server_port" {
description = "웹 서버가 리스닝할 포트"
type = number
default = 80
}
- user-data.sh.tpl
#!/bin/bash
# user-data.sh.tpl
# 웹 서버 설치 및 설정 (Apache2 설치)
echo "Installing Apache2 Web Server..."
sudo apt-get update -y
sudo apt-get install apache2 -y
# Apache2의 기본 웹 루트인 /var/www/html 디렉토리가 Apache2 설치 전에 없었다면 생성
# Apache2 설치가 성공하면 이 디렉토리는 이미 존재할 것이므로 오류는 나지 않습니다.
echo "Ensuring /var/www/html directory exists..."
sudo mkdir -p /var/www/html
# 인덱스 페이지 생성 및 동적 데이터 주입
# Apache2의 기본 웹 루트는 /var/www/html 입니다.
cat > /var/www/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>Web Server Info</title>
<style>
body { font-family: sans-serif; background-color: #f0f0f0; padding: 20px; }
.container { background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
h1 { color: #333; }
p { color: #555; }
strong { color: #007bff; }
</style>
</head>
<body>
<div class="container">
<h1>Hello from Terraform-managed Web Server!</h1>
<p>This server is running in <strong>${environment}</strong> environment.</p>
<p>Database Address: <strong>${db_address}</strong></p>
<p>Database Port: <strong>${db_port}</strong></p>
<p>Web Server Port: <strong>${server_port}</strong></p>
</div>
</body>
</html>
EOF
# Apache2 서비스 재시작 (user data 스크립트가 실행된 후)
# Apache2는 기본적으로 80번 포트에서 실행됩니다.
echo "Restarting Apache2 service..."
sudo systemctl restart apache2
sudo systemctl enable apache2 # VM 재부팅 시 자동 시작 설정
echo "Web server (Apache2) started on port ${server_port}."
main.tf 와 variables.tf 파일을 구성하고나서 테라폼 초기화 및 배포를 진행합니다.
# terraform 초기화
terraform init
# terraform 계획 확인
terraform plan
# terraform 적용 및 배포
terraform apply --auto-approve
이 프로젝트는 웹 서버 VM을 포함한 인프라를 배포하고, db-project의 출력 값을 user data에 주입하며, web-server-project.tfstate 파일을 Azure Blob Storage (tfstate-web 컨테이너)에 저장합니다.
6. 실습 결과 검증
1. 웹 서버 접근하여 웹 브라우저 접속

2. Azure Portal에서 Terraform 상태 파일 확인
직접적으로 확인하기위해서 blob에 익명 액세스를 허용하여 접근 가능하도록 하였습니다.

- tfstate-db

- tfstate-web

주의1)
Terraform에서 리소스를 destroy할 때는 의존성 역순으로 진행해야 합니다. 즉, 다른 리소스에 의해 참조되는 리소스를 먼저 삭제하면 안 됩니다. 이 경우, web-project가 db-project의 출력을 참조했으므로, web-project를 먼저 destroy했어야 합니다. 이러한 의존성 관리는 복잡한 인프라를 Terraform으로 관리할 때 매우 중요합니다.
주의2)
동적 데이터 처리를 위한 과정이긴하나, db-project의 output 정보를 바꿔서 배포한다고 한들 web-project의 웹 서버에는 바로 반영되지 않습니다. 바로 반영되게 하기 위해서는 CI/CD 환경을 구축해서 동적 데이터 업데이트를 진행해야합니다.
이유는 user_data의 한계입니다. user_data는 인프라 배초 초기 설정에는 매우 강력하지만, 인프라 배포 이후 런타임에 발생하는 동적 데이터 변경에는 한계가 있습니다.
# 예로 db_address와 db_port를 변경한다고해서 웹서버에 바로 반영되지 않습니다.
output "db_address" {
description = "가상 데이터베이스의 주소"
value = "db-server001.terzmy.com" # 실제 DB 주소라고 가정
}
output "db_port" {
description = "가상 데이터베이스의 포트"
value = 5433 # 실제 DB 포트라고 가정 (예: PostgreSQL)
}'Terraform' 카테고리의 다른 글
| [Terraform] 복잡한 조건문 (조건 분기) (4) | 2025.08.08 |
|---|---|
| [Terraform] 반복문 및 조건문 (2) | 2025.08.05 |
| [Terraform] 모듈 버전 관리 with Github (2) | 2025.08.04 |
| [Terraform] 테라폼 모듈 (2) | 2025.08.01 |
| [Terraform] 상태 파일(tfstate) 관리 (0) | 2025.07.31 |