Hub Network di una Landing Zone Azure con Terraform: la Virtual Network centrale e il piano di subnet
In una topologia hub & spoke, la Hub Virtual Network e’ la rete centrale che ospita i servizi condivisi dell’intera landing zone. Tutti gli spoke si agganciano ad essa tramite peering ed ereditano connettivita’, ispezione del traffico e risoluzione dei nomi, senza dover replicare componenti costosi. Progettare correttamente la VNet dell’hub e il suo piano di indirizzamento è il primo passo di ogni deployment di rete secondo il Microsoft Cloud Adoption Framework (CAF).
Questa guida descrive il deployment con Terraform della VNet hub e del resource group che la contiene, utilizzando gli Azure Verified Modules (AVM). La VNet e’ deployata nel resource group rg-hub-itn nella subscription Connectivity, region Italy North. Un aspetto centrale è l’esenzione dalla policy DDoS, che deve esistere prima della creazione della VNet.
Le guide dedicate a Azure Firewall, Azure Bastion e DNS completano il deployment dell’hub e presuppongono la VNet descritta qui.
Cos’e’ un hub e perche’ implementarlo
In una rete cloud di una certa dimensione, ogni applicazione o reparto tende ad avere la propria rete virtuale (VNet). Se ciascuna di queste reti gestisse da sola la connettivita’ verso Internet, verso l’on-premises e la sicurezza, si otterrebbe una proliferazione di componenti costosi e difficili da governare: un firewall per ogni rete, un gateway VPN per ogni rete, regole duplicate ovunque. L’hub nasce per risolvere questo problema.
L’hub e’ una rete virtuale centrale che ospita una sola volta i servizi condivisi da tutte le altre reti: il firewall che ispeziona il traffico, il servizio di accesso remoto alle macchine, la connettivita’ verso la sede aziendale, la risoluzione dei nomi. Le reti delle applicazioni, chiamate spoke (i raggi), si collegano all’hub tramite peering e utilizzano i suoi servizi senza doverli reinstallare. Il nome hub & spoke richiama proprio la ruota di una bicicletta: un mozzo centrale e tanti raggi che vi si connettono.
I motivi principali per implementare un hub sono:
- Risparmio – servizi costosi come Azure Firewall, VPN Gateway e Bastion vengono deployati una volta sola nell’hub invece che in ogni singola rete.
- Sicurezza centralizzata – tutto il traffico verso Internet e tra le applicazioni passa da un unico punto di controllo, dove si applicano e si verificano le regole.
- Governance – separare i servizi di rete in una subscription dedicata (Connectivity) permette di assegnare responsabilita’, permessi e costi in modo chiaro, come raccomandato dal Cloud Adoption Framework.
- Scalabilita’ – aggiungere una nuova applicazione significa creare un nuovo spoke e collegarlo all’hub, senza toccare l’architettura esistente.
Struttura del root module
L’hub e’ un unico root module Terraform, con ogni componente isolato nel proprio file. I file rilevanti per la rete sono main.tf (resource group), network.tf (VNet e subnet), policy-exemption.tf (esenzione DDoS), piu’ i file di configurazione comuni provider.tf, backend.tf, variables.tf, locals.tf e outputs.tf.
Provider e backend
Il file provider.tf dichiara i provider azurerm ~> 4.71 e azapi ~> 2.0, con Terraform >= 1.5. L’opzione storage_use_azuread abilita l’autenticazione Azure AD verso lo storage dello state.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~>4.71.0" } azapi = { source = "azure/azapi" version = "~> 2.0" } } required_version = ">= 1.5" } provider "azurerm" { subscription_id = var.azure_subscription_id storage_use_azuread = true features {} } provider "azapi" { subscription_id = var.azure_subscription_id } |
Il backend in backend.tf e’ volutamente vuoto: i parametri dello state remoto vengono passati all’init tramite il file state.config.
|
1 2 3 4 5 6 7 |
# Il blocco backend e' intenzionalmente vuoto: tutti i parametri # vengono passati al momento dell'init tramite il file state.config. # Comando per inizializzare: terraform init -backend-config="state.config" terraform { backend "azurerm" {} } |
Variabili e tag
Il file variables.tf centralizza nomi e prefissi. I default riflettono l’ambiente reale: VNet 10.0.0.0/22, resource group rg-hub-itn, region italynorth.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
variable "location" { description = "Regione Azure dove verranno create le risorse" type = string default = "italynorth" } variable "environment" { description = "Nome dell'ambiente, usato nei tag" type = string default = "prd" } variable "global_tags" { description = "Tag aggiuntivi da aggiungere a tutte le risorse" type = map(string) default = {} } variable "azure_subscription_id" { description = "ID della subscription Azure" type = string sensitive = true } variable "resource_group_name" { description = "Nome del resource group della hub, condiviso da network e firewall" type = string default = "rg-hub-itn" } # --- Network --- variable "vnet_name" { description = "Nome della hub virtual network" type = string default = "vnet-hub-itn" } variable "vnet_address_space" { description = "Spazio di indirizzi della hub virtual network" type = list(string) default = ["10.0.0.0/22"] } variable "firewall_subnet_prefix" { description = "CIDR della AzureFirewallSubnet, minimo /26 richiesto da Microsoft" type = string default = "10.0.0.0/26" } variable "bastion_subnet_prefix" { description = "CIDR della AzureBastionSubnet, minimo /26 richiesto da Microsoft" type = string default = "10.0.0.64/26" } variable "dns_inbound_subnet_prefix" { description = "CIDR della subnet inbound del Private DNS Resolver" type = string default = "10.0.1.0/28" } variable "dns_outbound_subnet_prefix" { description = "CIDR della subnet outbound del Private DNS Resolver" type = string default = "10.0.1.16/28" } # --- Azure Firewall --- variable "firewall_policy_name" { description = "Nome della firewall policy" type = string default = "afw-policy-hub-itn" } variable "firewall_name" { description = "Nome dell'Azure Firewall" type = string default = "afw-hub-itn" } # --- Azure Bastion --- variable "bastion_name" { description = "Nome dell'host Azure Bastion" type = string default = "bas-hub-itn" } # --- DNS --- variable "dns_resolver_name" { description = "Nome del Private DNS Resolver" type = string default = "dnsresolver-hub-itn" } |
Il file locals.tf costruisce i tag globali applicati a tutte le risorse, unendo i default con eventuali tag esterni.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# I tag vengono costruiti unendo quelli di default con eventuali tag # aggiuntivi passati dall'esterno tramite la variabile global_tags. locals { global_tags = merge( { Environment = var.environment ManagedBy = "Terraform" Project = "Azure-IaC" Scope = "Connectivity" }, var.global_tags ) } |
Il resource group
Il resource group rg-hub-itn e’ creato tramite il modulo AVM avm-res-resources-resourcegroup e contiene sia la VNet sia tutti gli altri servizi dell’hub.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Resource group unico che contiene sia la hub virtual network sia # Azure Firewall. Le altre risorse (network, firewall) sono definite # nei rispettivi file network.tf e afw.tf per mantenere il codice organizzato. module "resource_group" { source = "Azure/avm-res-resources-resourcegroup/azurerm" version = "0.2.2" name = var.resource_group_name location = var.location tags = local.global_tags enable_telemetry = false } |
Passo 1: L’esenzione dalla policy DDoS
Sul management group connectivity e’ assegnata la policy Enable-DDoS-VNET (effetto Modify) che obbliga ogni VNet ad agganciare uno specifico Azure DDoS Protection Plan, dal costo mensile elevato. Per un ambiente di dimostrazione si crea invece un’esenzione a livello di resource group.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# La policy "Enable-DDoS-VNET" e' assegnata (effetto Modify) sul management # group "connectivity" e obbliga ogni virtual network ad agganciare un # Azure DDoS Protection Plan specifico (rg-hub-ddos-italynorth/ddos-italynorth). # Quel piano ha un costo fisso mensile elevato e non e' giustificato per # questo ambiente di dimostrazione, quindi esentiamo il resource group # rg-hub-itn da questa specifica policy assignment invece di crearlo. # # Lo scope dell'esenzione e' il resource group (non la singola VNet) perche' # al momento della creazione della VNet la risorsa non esiste ancora e non # potrebbe quindi fare da scope per un'esenzione risorsa-per-risorsa. resource "azurerm_resource_group_policy_exemption" "ddos" { name = "exempt-ddos-${var.resource_group_name}" resource_group_id = module.resource_group.resource_id policy_assignment_id = "/providers/Microsoft.Management/managementGroups/connectivity/providers/Microsoft.Authorization/policyAssignments/Enable-DDoS-VNET" exemption_category = "Waiver" display_name = "Demo IaC - DDoS Protection Plan non necessario" description = "Ambiente di dimostrazione/guida per rg-hub-itn: nessun Azure DDoS Protection Plan deployato, costo (~2.700-3.000 EUR/mese) non giustificato per questo scope." } |
ATTENZIONE: L’esenzione ha scope sul resource group e non sulla singola VNet, perche’ al momento della creazione la VNet non esiste ancora. La VNet la referenzia come depends_on esplicita: senza l’esenzione gia’ presente, Azure Policy rifiuta la creazione con un errore 404 sul DDoS Protection Plan. Adattare il policy_assignment_id al proprio management group.
Passo 2: La Hub Virtual Network e il piano di subnet
La VNet vnet-hub-itn usa lo spazio 10.0.0.0/22 (1024 indirizzi) ed e’ creata con il modulo AVM avm-res-network-virtualnetwork. Il layout delle subnet e’ pianificato sull’intero /22 per lasciare spazio ai servizi futuri. Le subnet il cui nome e’ imposto da Azure (AzureFirewallSubnet, AzureBastionSubnet, GatewaySubnet) devono avere esattamente quel nome.
- AzureFirewallSubnet – 10.0.0.0/26 – ospita Azure Firewall (attiva).
- AzureBastionSubnet – 10.0.0.64/26 – ospita Azure Bastion (attiva).
- snet-dns-inbound – 10.0.1.0/28 – endpoint inbound del DNS Resolver, delegata (attiva).
- snet-dns-outbound – 10.0.1.16/28 – endpoint outbound del DNS Resolver, delegata (attiva).
- AzureFirewallManagementSubnet – 10.0.0.128/26 – forced tunneling (pianificata, commentata).
-
GatewaySubnet – 10.0.0.192/27 – VPN/ER Gateway (pianificata, commentata).12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182# La hub virtual network usa lo spazio 10.0.0.0/22 (1024 indirizzi).# Il layout delle subnet e' progettato per lasciare spazio ai servizi# che verranno aggiunti in futuro senza dover rifare il piano di indirizzamento.## Layout completo (attive e future):# AzureFirewallSubnet 10.0.0.0/26 (.0 - .63 ) 64 IP [attiva]# AzureBastionSubnet 10.0.0.64/26 (.64 - .127) 64 IP [attiva]# AzureFirewallManagementSubnet 10.0.0.128/26 (.128- .191) 64 IP [futuro, forced tunneling]# GatewaySubnet 10.0.0.192/27 (.192- .223) 32 IP [futuro]# snet-dns-inbound 10.0.1.0/28 (16 IP) [attiva]# snet-dns-outbound 10.0.1.16/28 (16 IP) [attiva]# Libero 10.0.1.32/27 in poimodule "vnet" {source = "Azure/avm-res-network-virtualnetwork/azurerm"version = "~> 0.8"name = var.vnet_namelocation = var.locationparent_id = module.resource_group.resource_idaddress_space = var.vnet_address_spacetags = local.global_tagsenable_telemetry = false# L'esenzione dalla policy Enable-DDoS-VNET deve esistere prima del tentativo# di creazione della VNet, altrimenti Azure Policy rifiuta la richiesta (404# sul DDoS Protection Plan non deployato). La dipendenza esplicita forza# Terraform a creare prima l'esenzione.depends_on = [azurerm_resource_group_policy_exemption.ddos]subnets = {# AzureFirewallSubnet deve chiamarsi esattamente cosi, Azure la riconosce per nome.# Il /26 e' il minimo richiesto da Microsoft per Azure Firewall.AzureFirewallSubnet = {name = "AzureFirewallSubnet"address_prefixes = [var.firewall_subnet_prefix]}# Le subnet seguenti sono pianificate ma non ancora deployate.# Per abilitarle in futuro basta decommentare il blocco corrispondente.# GatewaySubnet deve chiamarsi esattamente cosi, /27 e' il minimo raccomandato.# GatewaySubnet = {# name = "GatewaySubnet"# address_prefixes = ["10.0.0.192/27"]# }# AzureBastionSubnet deve chiamarsi esattamente cosi, Azure la riconosce per nome.# Il /26 e' il minimo richiesto da Microsoft per Azure Bastion.AzureBastionSubnet = {name = "AzureBastionSubnet"address_prefixes = [var.bastion_subnet_prefix]}# Le subnet del Private DNS Resolver devono essere delegate al servizio# Microsoft.Network/dnsResolvers. Non possono contenere altre risorse.# Una subnet separata per inbound (risoluzione da on-prem verso Azure)# e una per outbound (risoluzione da Azure verso on-prem o DNS custom).snet-dns-inbound = {name = "snet-dns-inbound"address_prefixes = [var.dns_inbound_subnet_prefix]delegations = [{name = "Microsoft.Network.dnsResolvers"service_delegation = {name = "Microsoft.Network/dnsResolvers"}}]}snet-dns-outbound = {name = "snet-dns-outbound"address_prefixes = [var.dns_outbound_subnet_prefix]delegations = [{name = "Microsoft.Network.dnsResolvers"service_delegation = {name = "Microsoft.Network/dnsResolvers"}}]}}}
NOTA: Le due subnet del DNS Resolver sono delegate al servizio Microsoft.Network/dnsResolvers e non possono contenere altre risorse. La depends_on verso l’esenzione DDoS garantisce l’ordine corretto di creazione.
Passo 3: Inizializzare, pianificare e applicare
Dalla cartella del root module, inizializzare Terraform passando la configurazione del backend, quindi generare e applicare il piano.
cd c:\code\iac\connectivity\rg-hub-itn
terraform init -backend-config=”state.config”

Figure 1 output di terraform init: il backend azurerm viene configurato e i provider inizializzati dal dependency lock file.
terraform plan -out tfplan
terraform apply tfplan
Passo 4: Verificare il resource group e la VNet
Nel portale Azure, navigare su Subscriptions > Connectivity > Resource groups. Il resource group rg-hub-itn deve comparire nella region Italy North, accanto al NetworkWatcherRG creato automaticamente da Azure.

Figure 2 il resource group rg-hub-itn nella subscription Connectivity, insieme al NetworkWatcherRG.
Aprendo il resource group, in fondo all’elenco delle risorse si trova la Virtual network vnet-hub-itn nella region Italy North. I tag Environment : prd e ManagedBy : Terraform confermano la gestione tramite Terraform.

Figure 3 in fondo alla lista delle risorse, la Virtual network vnet-hub-itn nella region Italy North.
Verifica del deployment
Il deployment della rete è completo quando:
- Il resource group rg-hub-itn esiste con i tag corretti.
- La VNet vnet-hub-itn (10.0.0.0/22) e’ in stato Succeeded.
- Le quattro subnet attive sono presenti con i prefissi e le delegation corretti.
COMPLETATO: La Hub Virtual Network e’ operativa. Su di essa possono ora essere deployati Azure Firewall, Azure Bastion e il DNS Private Resolver, e gli spoke possono agganciarsi via peering.
Conclusioni
Abbiamo deployato con Terraform e Azure Verified Modules il fondamento della rete hub: il resource group, l’esenzione DDoS e la Virtual Network con il suo piano di subnet dimensionato per la crescita futura. Gestire la rete come root module con state remoto garantisce ripetibilita’ e versionamento.
NOTA: Come evoluzione, abilitare le subnet GatewaySubnet e AzureFirewallManagementSubnet gia’ pianificate quando si aggiungono connettivita’ ibrida e forced tunneling.