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

빠른 시작

  1. 로그인 및 구독 선택
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"]
    }
  } )
}

Leave a Reply

Your email address will not be published. Required fields are marked *

error: Content is protected !!