Terraform on Azure [모듈화] - (3) network 모듈
배포 계획
구조: Hub-Spoke 네트워크 모델
- Hub: 공통 인프라 구성 (VPN Gateway, Firewall 등)
- Spoke: 업무별 구분된 네트워크 (Web, WAS, DB 등 리소스 배치)
보안 구성
- NSG (Network Security Group): 각 Spoke VNet 및 서브넷에 할당
- VPN Gateway: Hub VNet에 배치
- Firewall: Hub VNet에 배치 및 UDR과 연동
리소스 그룹 구조
- RG-NETWORK – 가상 네트워크 및 서브넷 등 네트워크 리소스
- RG-SECURITY – NSG, Firewall, VPN 등 보안 리소스
- RG-COMPUTE – 가상 머신 등 컴퓨트 리소스
- RG-STORAGE – Storage Account 등 저장소 리소스
Tag 구성
- Key : Value 예시
- ResourceName : vm-web-prd-01 등
- ServiceType : WEB, WAS, DB 등
- Environment : PRD, QA, DEV
- DeploymentDate : 2025-05-07 등
1. 모듈 설계
- 생성할 Azure 리소스 단위로 모듈 구분
- network / compute / storage / security 등으로 모듈을 구분했더니, 한 개 모듈 내에서 순환참조 오류가 발생함
- environments 구분은 next stage로 남겨두고 이번에는 제외
📁 terraform/
│
├── 📁 modules/
│ ├── 📁 resource_group/ # 리소스 그룹 생성
│ ├── 📁 vnet/ # VNet 및 Subnet 생성 (Hub / Spoke 공통)
│ ├── 📁 subnet/ # Subnet 생성
│ ├── 📁 vnet_peering/ # Hub ↔ Spoke 피어링
│ ├── 📁 nsg/ # NSG 생성 및 Subnet에 연결
│ ├── 📁 vpn # VPN Gateway (Hub 전용)
│ ├── 📁 firewall/ # Azure Firewall (Hub 전용)
│ ├── 📁 udr/ # UDR (Firewall 경유 라우팅)
│ ├── 📁 vm/ # 가상 머신 배포 (Web, WAS, DB 등)
│ └── 📁 storage_account/ # 스토리지 계정
│
├── 📄 main.tf # 상태파일 저장 위치 (예: Azure Storage)
├── 📄 provider.tf # Azure Provider 정의
└── 📄 variables.tf # 공통 변수 정의
작업순서 요약
- Child Module 생성
- /terraform/modules 하위 디렉토리 만들기
- 각각에 main.tf, variables.tf, outputs.tf 만들기 (여기서 모듈 tf 파일 내용은 모두 main.tf 하나에 몰아넣어 트러블슈팅을 용이하게 하였음)
- 재사용에 용이한 for_each는 child 모듈 내에서 사용
- Parent Module 작성
- main.tf에 각 모듈을 module "XXX" { ... } 형식으로 호출
- 필요한 변수들을 variables.tf로 전달
- 적용 전 테스트
- terraform init → plan → apply
2. Resource Group 모듈
** 구조의 기반이 되는 Child module 먼저 작성
- main.tf
### 모듈 구성 ###
// Parent (루트): 단순히 resource_groups map을 모듈에 전달
// Child (모듈): for_each로 반복하면서 여러 리소스 그룹 생성
### 모듈 작성 문법 ###
# module "모듈명" {
# source = "모듈_경로"
# 변수1 = 값
# 변수2 = {
# key1 = "값1"
# key2 = ["리스트값1", "리스트값2"]
# }
# }
module "resource_groups" {
source = "./modules/resource_group"
resource_groups = {
network = {
name = "LJY-RG-NETWORK"
location = var.location
tags = {
DeploymentDate = "2025-05-22"
}
},
security = {
name = "LJY-RG-SECURITY"
location = var.location
tags = {
DeploymentDate = "2025-05-22"
}
},
compute = {
name = "LJY-RG-COMPUTE"
location = var.location
tags = {
DeploymentDate = "2025-05-22"
}
},
storage = {
name = "LJY-RG-STORAGE"
location = var.location
tags = {
DeploymentDate = "2025-05-22"
}
}
}
}
- variables.tf
variable "location" {
description = "Azure region to deploy all resources"
type = string
default = "koreacentral"
}
- ./modules/resource_group 모듈
->> main.tf에 variables과 outputs 함께 입력.
->> parent가 아닌 child module에서 for_each 사용
resource "azurerm_resource_group" "rg" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
variable "resource_groups" {
description = "Map of resource groups"
type = map(object({
name = string
location = string
tags = map(string)
}))
}
output "resource_group_names" {
value = { for key, rg in azurerm_resource_group.rg : key => rg.name }
}
##각각의 리소스 그룹에 대해 key(k)와 value(rg)를 꺼내서
# key는 그대로 두고, value는 해당 리소스 그룹의 이름만 꺼냄.
# 결과적으로 [key] = 리소스 그룹 이름 의 형태인 map이 생성됩니다.
# k => rg.name → map 형식으로 반복
** 새 모듈 추가시 init 필요, tf별로 저장 누락 없도록 주의할 것
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
콘솔에서 리소스 배포 확인
3. Vnet 모듈
- main.tf
module "vnet" {
source = "./modules/vnet"
location = var.location
vnets = {
hub = {
name = "vnet-hub"
address_space = ["10.0.0.0/16"]
resource_group_name = module.resource_groups.resource_group_names["security"]
tags = {
DeploymentDate = "2025-05-07"
}
}
prd = {
name = "vnet-prd"
address_space = ["10.1.0.0/16"]
resource_group_name = module.resource_groups.resource_group_names["network"]
tags = {
DeploymentDate = "2025-05-07"
}
}
dev = {
name = "vnet-dev"
address_space = ["10.2.0.0/16"]
resource_group_name = module.resource_groups.resource_group_names["network"]
tags = {
DeploymentDate = "2025-05-07"
}
}
}
}
- variables.tf
variable "vnets" {
description = "Map of VNets and subnets"
type = map(object({
name = string
address_space = list(string)
resource_group_name = string
tags = map(string)
}))
}
- 모듈 main.tf
resource "azurerm_resource_group" "rg" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
variable "resource_groups" {
description = "Map of resource groups"
type = map(object({
name = string
location = string
tags = map(string)
}))
}
output "resource_group_names" {
value = { for key, rg in azurerm_resource_group.rg : key => rg.name }
}
##각각의 리소스 그룹에 대해 key(k)와 value(rg)를 꺼내서
# key는 그대로 두고, value는 해당 리소스 그룹의 이름만 꺼냄.
# 결과적으로 [key] = 리소스 그룹 이름 의 형태인 map이 생성됩니다.
# k => rg.name → map 형식으로 반복
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
콘솔에서 리소스 배포 확인
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
콘솔에서 리소스 배포 확인
4. Subnet 모듈
- main.tf
module "subnet" {
source = "./modules/subnet"
subnets = {
gateway = {
name = "GatewaySubnet"
resource_group_name = module.resource_groups.resource_group_names["security"]
virtual_network_name = module.vnet.vnet_names["hub"]
address_prefix = "10.0.1.0/27"
}
firewall = {
name = "AzureFirewallSubnet"
resource_group_name = module.resource_groups.resource_group_names["security"]
virtual_network_name = module.vnet.vnet_names["hub"]
address_prefix = "10.0.2.0/26"
}
prd_web = {
name = "prd-web-subnet"
resource_group_name = module.resource_groups.resource_group_names["network"]
virtual_network_name = module.vnet.vnet_names["prd"]
address_prefix = "10.1.1.0/24"
}
prd_db = {
name = "prd-db-subnet"
resource_group_name = module.resource_groups.resource_group_names["network"]
virtual_network_name = module.vnet.vnet_names["prd"]
address_prefix = "10.1.2.0/24"
}
dev_web = {
name = "dev-web-subnet"
resource_group_name = module.resource_groups.resource_group_names["network"]
virtual_network_name = module.vnet.vnet_names["dev"]
address_prefix = "10.2.1.0/24"
}
dev_db = {
name = "dev-db-subnet"
resource_group_name = module.resource_groups.resource_group_names["network"]
virtual_network_name = module.vnet.vnet_names["dev"]
address_prefix = "10.2.2.0/24"
}
}
}
- 모듈 main.tf
variable "subnets" {
description = "Map of subnet objects"
type = map(object({
name = string
resource_group_name = string
virtual_network_name = string
address_prefix = string
}))
default = {} ## 기본 값 넣어주지 않으면 별도로 입력해줘야해서 default ={} 입력
}
resource "azurerm_subnet" "this" {
for_each = var.subnets
name = each.value.name
resource_group_name = each.value.resource_group_name
virtual_network_name = each.value.virtual_network_name
address_prefixes = [each.value.address_prefix]
}
** var에 기본값(default = {})이 없으면, terraform plan 시 반드시 CLI나 terraform.tfvars 또는 -var 옵션으로 값을 넣어줘야함
따라서 var에서는 default = {}과 같이 기본값을 포함시킴
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
5. Vnet_Peering 모듈
- main.tf
module "vnet_peering" {
source = "./modules/vnet_peering"
peerings = {
hub-prd = {
name_local = "hub-to-prd"
name_remote = "prd-to-hub"
rg_local = module.resource_groups.resource_group_names["security"]
rg_remote = module.resource_groups.resource_group_names["network"]
vnet_name_local = module.vnet.vnet_names["hub"]
vnet_name_remote = module.vnet.vnet_names["prd"]
vnet_id_local = module.vnet.vnet_ids["hub"]
vnet_id_remote = module.vnet.vnet_ids["prd"]
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
hub-dev = {
name_local = "hub-to-dev"
name_remote = "dev-to-hub"
rg_local = module.resource_groups.resource_group_names["security"]
rg_remote = module.resource_groups.resource_group_names["network"]
vnet_name_local = module.vnet.vnet_names["hub"]
vnet_name_remote = module.vnet.vnet_names["dev"]
vnet_id_local = module.vnet.vnet_ids["hub"]
vnet_id_remote = module.vnet.vnet_ids["dev"]
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
}
}
- 모듈 main.tf
variable "peerings" {
description = "Map of vnet peering definitions"
type = map(object({
name_local = string
name_remote = string
rg_local = string
rg_remote = string
vnet_name_local = string
vnet_name_remote = string
vnet_id_local = string
vnet_id_remote = string
allow_forwarded_traffic = bool
allow_gateway_transit = bool
use_remote_gateways = bool
}))
default = {}
}
resource "azurerm_virtual_network_peering" "local_to_remote" {
for_each = var.peerings
name = each.value.name_local
resource_group_name = each.value.rg_local
virtual_network_name = each.value.vnet_name_local
remote_virtual_network_id = each.value.vnet_id_remote
allow_forwarded_traffic = each.value.allow_forwarded_traffic
allow_gateway_transit = each.value.allow_gateway_transit
use_remote_gateways = each.value.use_remote_gateways
}
resource "azurerm_virtual_network_peering" "remote_to_local" {
for_each = var.peerings
name = each.value.name_remote
resource_group_name = each.value.rg_remote
virtual_network_name = each.value.vnet_name_remote
remote_virtual_network_id = each.value.vnet_id_local
allow_forwarded_traffic = each.value.allow_forwarded_traffic
allow_gateway_transit = each.value.allow_gateway_transit
use_remote_gateways = each.value.use_remote_gateways
}
# output "peering_names" {
# value = {
# for k, v in azurerm_virtual_network_peering.local_to_remote :
# k => v.name
# }
# }
- vnet 모듈 outputs 추가필요
: peering에서 vnet id를 참조하기 때문에 추가
# Vnet Peering 참조용
output "vnet_ids" {
value = { for k, v in azurerm_virtual_network.vnet : k => v.id }
description = "Map of virtual network IDs keyed by vnet identifier"
}
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )