
Azure Hub–Spoke Landing Zone for SMB (Terraform)
중소·중견 조직(SMB)을 위한 간단하고 견고한 Azure Hub–Spoke 네트워크 레퍼런스입니다. 허브/스포크 VNet, 서브넷, NSG, NSG 연동, 양방향 VNet 피어링을 Terraform으로 구성합니다.
목표: 엔터프라이즈급 LZ의 핵심 이점(격리/확장/공유 서비스)을 유지하면서도, 단순한 변수만으로 빠르게 배포할 수 있도록 설계
+----------------------------+
| Hub VNet |
| (공유 서비스/보안/Egress) |
| |
Internet -> | AzureFirewallSubnet |
| GatewaySubnet (VPN/ER) |
| Management-Subnet |
| Shared-Subnet |
+-------------+--------------+
| Peering (bi-directional)
|
+----------------------+----------------------+
| | |
+------+-------+ +-----+-------+ +-----+-------+
| Spoke (prod) | | Spoke (stg) | | Spoke (dev) |
| default snet | | default snet| | default snet|
+--------------+ +-------------+ +-------------+
https://github.com/cloudtraveler/Azure-Hub-Spoke-Landing-Zone-for-SMB
빠른 시작
- 로그인 및 구독 선택
az login
az account set --subscription "<SUBSCRIPTION_ID_OR_NAME>"
2. Terraform 초기화/검증:
terraform init
terraform plan -var 'product_name=myapp'
3. 적용:
terraform apply -var 'product_name=myapp'
main.tf
# Creating resource groups for each VNET
# 각 VNET별로 Resource Group 생성
resource "azurerm_resource_group" "rg-spoke" {
for_each = var.vnets
name = "rg-${each.value.name}-${var.product_name}"
location = var.location
tags = {
source = "terraform"
}
}
# Creating the VNETs
# 각 VNET 리소스 생성
resource "azurerm_virtual_network" "vnet-spoke" {
for_each = var.vnets
name = "vnet-${each.value.name}-${var.product_name}"
address_space = [each.value.address_space]
location = azurerm_resource_group.rg-spoke[each.key].location
resource_group_name = azurerm_resource_group.rg-spoke[each.key].name
tags = {
source = "terraform"
}
# Creating the subnets under hub VNET
# Hub VNET에 Subnet 생성
dynamic "subnet" {
for_each = each.key == keys(var.vnets)[0] ? local.subnets_hub : tomap({})
content {
name = subnet.key
address_prefixes = subnet.value.address_prefixes
}
}
# Creating the subnets under prod VNET
# Prod VNET에 Subnet 생성
dynamic "subnet" {
for_each = each.key == keys(var.vnets)[1] ? local.subnets_prod : {}
content {
name = subnet.key
address_prefixes = subnet.value.address_prefixes
}
}
# Creating the subnets under staging VNET
# Staging VNET에 Subnet 생성
dynamic "subnet" {
for_each = each.key == keys(var.vnets)[2] ? local.subnets_staging : {}
content {
name = subnet.key
address_prefixes = subnet.value.address_prefixes
}
}
# Creating the subnets under dev VNET
# Dev VNET에 Subnet 생성
dynamic "subnet" {
for_each = each.key == keys(var.vnets)[3] ? local.subnets_dev : {}
content {
name = subnet.key
address_prefixes = subnet.value.address_prefixes
}
}
}
# Creating the Network Security Groups
# 각 VNET별 Network Security Group(NSG) 생성
resource "azurerm_network_security_group" "nsg" {
for_each = var.vnets
name = "nsg-${each.value.name}-${var.product_name}"
location = var.location
resource_group_name = azurerm_resource_group.rg-spoke[each.key].name
# Rule: Deny SSH inbound
# 규칙: SSH 인바운드 차단
security_rule {
name = "ssh"
priority = 100
direction = "Inbound"
access = "Deny"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Rule: Deny RDP inbound
# 규칙: RDP 인바운드 차단
security_rule {
name = "rdp"
priority = 110
direction = "Inbound"
access = "Deny"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Rule: Deny HTTP inbound
# 규칙: HTTP 인바운드 차단
security_rule {
name = "http"
priority = 120
direction = "Inbound"
access = "Deny"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Rule: Deny HTTPS inbound
# 규칙: HTTPS 인바운드 차단
security_rule {
name = "https"
priority = 130
direction = "Inbound"
access = "Deny"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Rule: Allow all intra-VNET inbound traffic
# 규칙: VNET 내부 인바운드 허용
security_rule {
name = "allow_all_in_vnet_traffic"
priority = 140
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
# Rule: Allow all intra-VNET outbound traffic
# 규칙: VNET 내부 아웃바운드 허용
security_rule {
name = "allow_all_out_vnet_traffic"
priority = 150
direction = "Outbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
}
outputs.tf
output "subnet_ids" {
value = { for v in azurerm_virtual_network.vnet-spoke : v.name => [for s in v.subnet : s.id] }
description = "The IDs of the subnets"
}
output "nsg_ids" {
value = { for n in azurerm_network_security_group.nsg : n.name => n.id }
description = "The IDs of the NSGs"
}
output "vnet_spoke_ids" {
value = { for v in azurerm_virtual_network.vnet-spoke : v.name => v.id }
description = "The IDs of the spoke VNets"
}
output "resource_group_ids" {
value = { for r in azurerm_resource_group.rg-spoke : r.name => r.id }
description = "The IDs of the resource groups"
}
output "product-name" {
value = var.product_name
description = "The product name"
}
provider.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.110.0"
}
}
}
provider "azurerm" {
features {}
# Azure 인증은 Azure CLI (az login) or ARM_* environment variables.
# 필요하면 ID값 환경 설정 set ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, and ARM_SUBSCRIPTION_ID.
# subscription_id = " "
# tenant_id = " "
}
variables.tf
variable "location" {
type = string
description = "azure resources location"
default = "Korea Central"
}
variable "product_name" {
type = string
nullable = false
# description = "(Mandatory) Project/Application name. e.g WebApp-net \nThis will be used as prefix for all resources created."
description = "(필수) 프로젝트/애플리케이션 이름. 예: WebApp \n 이 이름은 생성되는 모든 리소스의 접두사로 사용됩니다."
}
variable "vnets" {
description = "Map of vnets to create"
type = map(object({
name = string
address_space = string
}))
default = {
spoke1 = { name = "hub", address_space = "10.0.0.0/20" },
spoke2 = { name = "prod", address_space = "10.1.0.0/16" },
spoke3 = { name = "staging", address_space = "10.2.0.0/16" },
spoke4 = { name = "dev", address_space = "10.3.0.0/16" }
}
}
locals {
subnets_hub = tomap ({
"Management-Subnet" = {
address_prefixes = ["10.0.1.0/24"]
},
"Gateway-Subnet" = {
address_prefixes = ["10.0.15.224/27"]
},
"Shared-Subnet" = {
address_prefixes = ["10.0.4.0/22"]
},
"AzureFirewall-Subnet" = {
address_prefixes = ["10.0.15.0/26"]
}
} )
subnets_dev = tomap ({
"default" = {
address_prefixes = ["10.3.1.0/24"]
}
} )
subnets_prod = tomap ({
"default" = {
address_prefixes = ["10.1.1.0/24"]
}
} )
subnets_staging = tomap ({
"default" = {
address_prefixes = ["10.2.1.0/24"]
}
} )
}