Cloud/AZURE

Terraform on Azure [모듈화] - (3) network 모듈

BOKCH1 2025. 5. 22. 19:15

배포 계획

구조: 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 자동 입력 )