Hub-Spoke Landing Zone com Terraform
Neste artigo você vai criar uma Hub-Spoke Landing Zone com Terraform no Azure, provisionando quatro VNets, Resource Groups por região e camada, subnets segmentadas e seis peerings entre as regiões Brazil South e East US. A hub-spoke landing zone terraform é a base sobre a qual todos os serviços de Disaster Recovery da série serão construídos — Firewall, Bastion, ASR e AKS.
- 📖 Art. 01: Planejamento de DR Azure e Landing Zone
- ⚙️ Art. 02 (este): Hub-Spoke Landing Zone com Terraform
- 🔒 Art. 03: Azure Firewall e NSGs na Landing Zone
- 🔒 Art. 04: Azure Bastion na Landing Zone sem IP Público
- 🔒 Art. 05: Azure Front Door e Traffic Manager para Failover
- 🔒 Art. 06: DNS Privado Multi-Região no Azure
- 🔒 Art. 07: Azure Site Recovery para VMs Multi-Região
- 🔒 Art. 08: Azure Storage com Geo-Replicação para DR
- 🔒 Art. 09: AKS Multi-Região com Failover no Azure
- 🔒 Art. 10: Velero no AKS para Backup e Restore Cross-Region
- 🔒 Art. 11: Runbooks de Failover no Azure Automation
- 🔒 Art. 12: Simular um DR no Azure sem Impacto em Produção
- 🔒 Art. 13: Monitoramento do DR no Azure com Azure Monitor
Sumário
- O que é a topologia Hub-Spoke
- Arquitetura do lab
- Pré-requisitos
- Variáveis de ambiente
- Passo 1 — Configurar o providers.tf
- Passo 2 — Variables e Locals
- Passo 3 — Resource Groups, VNets e Subnets
- Passo 4 — VNet Peerings
- Passo 5 — Outputs
- Passo 6 — Executar o Terraform
- Verificar a configuração
- Troubleshooting
- Limpeza dos recursos
- Próximos passos
O que é a topologia Hub-Spoke
A topologia hub-spoke é o padrão de rede recomendado pela Microsoft para Landing Zones corporativas no Azure. Na prática, uma Hub-Spoke Landing Zone com Terraform permite descrever toda essa arquitetura como código versionado e reproduzível. A VNet central — o Hub — concentra os serviços compartilhados de segurança e conectividade: Azure Firewall, Azure Bastion, VPN Gateway e Private DNS Zones. As VNets periféricas — os Spokes — hospedam os workloads isolados, acessando os serviços do Hub via VNet Peering.
Para suportar Disaster Recovery, a Hub-Spoke Landing Zone com Terraform precisa ser replicada em duas regiões: a primária (Brazil South) e a secundária (East US). Um Global VNet Peering Hub-to-Hub conecta as duas regiões, permitindo comunicação entre workloads primários e secundários quando necessário durante um failover.
Portanto, a estrutura completa da hub-spoke landing zone com Terraform fica assim: dois Hubs regionais conectados entre si por Global Peering e, em cada região, um Spoke conectado ao Hub local via peering regional. Essa topologia é a fundação sobre a qual deployaremos Firewall, Bastion, ASR e AKS nos artigos seguintes da série.
Arquitetura do lab
A seguir estão todos os recursos criados neste artigo para a hub-spoke landing zone com Terraform, organizados por categoria. No total são 30 recursos: 4 Resource Groups, 4 VNets, 16 subnets e 6 peerings.
Resource Groups
| Nome | Região | Finalidade |
|---|---|---|
| rg-drlab-network-brazilsouth | Brazil South | Hub VNet primário e subnets |
| rg-drlab-network-eastus | East US | Hub VNet secundário e subnets |
| rg-drlab-workload-brazilsouth | Brazil South | Spoke VNet primário e workloads |
| rg-drlab-workload-eastus | East US | Spoke VNet secundário e workloads |
VNets e Address Spaces
| Nome | Tipo | Address Space | Região |
|---|---|---|---|
| vnet-hub-drlab-brazilsouth | Hub | 10.0.0.0/16 | Brazil South |
| vnet-hub-drlab-eastus | Hub | 10.1.0.0/16 | East US |
| vnet-spoke-drlab-brazilsouth | Spoke | 10.2.0.0/16 | Brazil South |
| vnet-spoke-drlab-eastus | Spoke | 10.3.0.0/16 | East US |
Subnets do Hub (por região)
Três subnets do Hub têm nomes obrigatórios definidos pelo Azure — qualquer variação causa erro de validação no terraform apply. Além disso, AzureFirewallSubnet e AzureBastionSubnet exigem prefixo mínimo de /26 para o SKU Standard:
| Subnet | CIDR (Brazil South) | CIDR (East US) | Observação |
|---|---|---|---|
| AzureFirewallSubnet | 10.0.0.0/26 | 10.1.0.0/26 | Nome obrigatório, mín. /26 |
| GatewaySubnet | 10.0.0.64/27 | 10.1.0.64/27 | Nome obrigatório para VPN/ExpressRoute |
| AzureBastionSubnet | 10.0.0.128/26 | 10.1.0.128/26 | Nome obrigatório, mín. /26 (Standard) |
| snet-management | 10.0.0.192/27 | 10.1.0.192/27 | Jump servers de gerência |
Subnets do Spoke (por região)
| Subnet | CIDR (Brazil South) | CIDR (East US) | Finalidade |
|---|---|---|---|
| snet-frontend | 10.2.1.0/24 | 10.3.1.0/24 | Balanceadores e APIs |
| snet-backend | 10.2.2.0/24 | 10.3.2.0/24 | Microsserviços e lógica de negócio |
| snet-data | 10.2.3.0/24 | 10.3.3.0/24 | Bancos de dados e storage |
| snet-aks | 10.2.4.0/22 | 10.3.4.0/22 | Nós e pods do AKS (1022 hosts) |
VNet Peerings
O VNet Peering no Azure é unidirecional — cada par de VNets exige dois recursos, um em cada direção. Para os três pares desta Landing Zone, são necessários seis peerings no total:
| Peering | Direção | Tipo |
|---|---|---|
| peer-hub-primary-to-hub-secondary | BRS → EUS | Global VNet Peering |
| peer-hub-secondary-to-hub-primary | EUS → BRS | Global VNet Peering |
| peer-hub-primary-to-spoke-primary | Hub BRS → Spoke BRS | Regional |
| peer-spoke-primary-to-hub-primary | Spoke BRS → Hub BRS | Regional |
| peer-hub-secondary-to-spoke-secondary | Hub EUS → Spoke EUS | Regional |
| peer-spoke-secondary-to-hub-secondary | Spoke EUS → Hub EUS | Regional |
Pré-requisitos
Segurança: Os comandos deste artigo utilizam variáveis de ambiente para credenciais. Nunca insira IDs de subscription, senhas ou chaves diretamente nos comandos. Use um arquivo .env local (não versionado) ou o Azure Key Vault para armazenar segredos.
- Terraform ≥ 1.5 instalado — verificar com
terraform -version - Azure CLI instalado e autenticado — verificar com
az account show - Subscription Azure com permissão de Contributor ou Owner
- Git para clonar o repositório com os arquivos Terraform
- Leitura do Art. 01 — Planejamento de DR Azure e Landing Zone para entender a topologia
Variáveis de ambiente
O único dado sensível passado ao Terraform neste módulo é o subscription_id. Em vez de hardcodá-lo no terraform.tfvars, use a variável de ambiente com prefixo TF_VAR_, que o Terraform reconhece automaticamente sem nenhuma configuração adicional:
# Exportar antes de qualquer comando terraform
export TF_VAR_subscription_id="<SUBSCRIPTION_ID>"
# Confirmar que foi exportada corretamente
echo $TF_VAR_subscription_id
Alternativamente, copie o arquivo terraform.tfvars.example para terraform.tfvars e preencha os valores reais localmente. O arquivo terraform.tfvars está no .gitignore e nunca será commitado no repositório.
Passo 1 — Configurar o providers.tf
O providers.tf define a versão mínima do Terraform e do provider AzureRM. A constraint ~> 4.0 permite upgrades de patch dentro da versão 4, garantindo compatibilidade enquanto recebe correções de segurança automaticamente:
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
features {}
}
Passo 2 — Variables e Locals
O variables.tf declara todas as entradas parametrizáveis da hub-spoke landing zone com Terraform. A variável subscription_id é marcada como sensitive = true para que o Terraform não a exiba nos logs de plano e apply:
variable "subscription_id" {
description = "ID da subscription Azure"
type = string
sensitive = true
}
variable "project" {
description = "Nome curto do projeto — usado nos nomes dos recursos"
type = string
default = "drlab"
}
variable "region_primary" {
description = "Região primária Azure"
type = string
default = "brazilsouth"
}
variable "region_secondary" {
description = "Região secundária Azure"
type = string
default = "eastus"
}
variable "hub_primary_address_space" {
description = "Address space do Hub VNet primário"
type = string
default = "10.0.0.0/16"
}
variable "hub_secondary_address_space" {
description = "Address space do Hub VNet secundário"
type = string
default = "10.1.0.0/16"
}
variable "spoke_primary_address_space" {
description = "Address space do Spoke VNet primário"
type = string
default = "10.2.0.0/16"
}
variable "spoke_secondary_address_space" {
description = "Address space do Spoke VNet secundário"
type = string
default = "10.3.0.0/16"
}
Em seguida, o locals.tf centraliza toda a convenção de nomenclatura. Dessa forma, alterar o valor da variável project atualiza automaticamente os nomes de todos os recursos, sem precisar editar cada bloco individualmente:
locals {
tags = {
project = var.project
serie = "dr-azure-landing-zone"
managed_by = "terraform"
}
rg_network_primary = "rg-${var.project}-network-${var.region_primary}"
rg_network_secondary = "rg-${var.project}-network-${var.region_secondary}"
rg_workload_primary = "rg-${var.project}-workload-${var.region_primary}"
rg_workload_secondary = "rg-${var.project}-workload-${var.region_secondary}"
hub_primary_name = "vnet-hub-${var.project}-${var.region_primary}"
hub_secondary_name = "vnet-hub-${var.project}-${var.region_secondary}"
spoke_primary_name = "vnet-spoke-${var.project}-${var.region_primary}"
spoke_secondary_name = "vnet-spoke-${var.project}-${var.region_secondary}"
}
Passo 3 — Resource Groups, VNets e Subnets
O main.tf provisiona os Resource Groups, as quatro VNets e todas as subnets desta hub-spoke landing zone com Terraform. O trecho abaixo mostra a criação do Hub primário (Brazil South) e do Spoke primário — o padrão se repete simetricamente para a região secundária:
# Resource Groups
resource "azurerm_resource_group" "network_primary" {
name = local.rg_network_primary
location = var.region_primary
tags = local.tags
}
resource "azurerm_resource_group" "workload_primary" {
name = local.rg_workload_primary
location = var.region_primary
tags = local.tags
}
# Hub VNet — Brazil South
resource "azurerm_virtual_network" "hub_primary" {
name = local.hub_primary_name
resource_group_name = azurerm_resource_group.network_primary.name
location = azurerm_resource_group.network_primary.location
address_space = [var.hub_primary_address_space]
tags = local.tags
}
resource "azurerm_subnet" "hub_primary_firewall" {
name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
address_prefixes = ["10.0.0.0/26"]
}
resource "azurerm_subnet" "hub_primary_gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
address_prefixes = ["10.0.0.64/27"]
}
resource "azurerm_subnet" "hub_primary_bastion" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
address_prefixes = ["10.0.0.128/26"]
}
resource "azurerm_subnet" "hub_primary_management" {
name = "snet-management"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
address_prefixes = ["10.0.0.192/27"]
}
# Spoke VNet — Brazil South
resource "azurerm_virtual_network" "spoke_primary" {
name = local.spoke_primary_name
resource_group_name = azurerm_resource_group.workload_primary.name
location = azurerm_resource_group.workload_primary.location
address_space = [var.spoke_primary_address_space]
tags = local.tags
}
resource "azurerm_subnet" "spoke_primary_frontend" {
name = "snet-frontend"
resource_group_name = azurerm_resource_group.workload_primary.name
virtual_network_name = azurerm_virtual_network.spoke_primary.name
address_prefixes = ["10.2.1.0/24"]
}
resource "azurerm_subnet" "spoke_primary_backend" {
name = "snet-backend"
resource_group_name = azurerm_resource_group.workload_primary.name
virtual_network_name = azurerm_virtual_network.spoke_primary.name
address_prefixes = ["10.2.2.0/24"]
}
resource "azurerm_subnet" "spoke_primary_data" {
name = "snet-data"
resource_group_name = azurerm_resource_group.workload_primary.name
virtual_network_name = azurerm_virtual_network.spoke_primary.name
address_prefixes = ["10.2.3.0/24"]
}
resource "azurerm_subnet" "spoke_primary_aks" {
name = "snet-aks"
resource_group_name = azurerm_resource_group.workload_primary.name
virtual_network_name = azurerm_virtual_network.spoke_primary.name
address_prefixes = ["10.2.4.0/22"]
}
Observe que o snet-aks usa um prefixo /22, o que equivale a 1022 endereços utilizáveis. Isso é necessário porque o plugin Azure CNI atribui um IP por pod diretamente na subnet — workloads com muitos pods esgotam ranges menores rapidamente.
Passo 4 — VNet Peerings
Com as VNets da hub-spoke landing zone com Terraform criadas, o arquivo peerings.tf define os seis peerings. O peering Hub-to-Hub entre regiões diferentes é classificado pelo Azure como Global VNet Peering e tem custo de transferência de dados — por isso, no contexto de DR, ele é usado principalmente para replicação assíncrona e failover, não para tráfego de aplicação em tempo real:
# Global VNet Peering — Hub BRS ↔ Hub EUS
resource "azurerm_virtual_network_peering" "hub_primary_to_hub_secondary" {
name = "peer-hub-primary-to-hub-secondary"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
remote_virtual_network_id = azurerm_virtual_network.hub_secondary.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
resource "azurerm_virtual_network_peering" "hub_secondary_to_hub_primary" {
name = "peer-hub-secondary-to-hub-primary"
resource_group_name = azurerm_resource_group.network_secondary.name
virtual_network_name = azurerm_virtual_network.hub_secondary.name
remote_virtual_network_id = azurerm_virtual_network.hub_primary.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
# Peering Regional — Hub BRS ↔ Spoke BRS
resource "azurerm_virtual_network_peering" "hub_primary_to_spoke_primary" {
name = "peer-hub-primary-to-spoke-primary"
resource_group_name = azurerm_resource_group.network_primary.name
virtual_network_name = azurerm_virtual_network.hub_primary.name
remote_virtual_network_id = azurerm_virtual_network.spoke_primary.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
resource "azurerm_virtual_network_peering" "spoke_primary_to_hub_primary" {
name = "peer-spoke-primary-to-hub-primary"
resource_group_name = azurerm_resource_group.workload_primary.name
virtual_network_name = azurerm_virtual_network.spoke_primary.name
remote_virtual_network_id = azurerm_virtual_network.hub_primary.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
allow_gateway_transit = false
use_remote_gateways = false
}
O parâmetro allow_forwarded_traffic = true é essencial para que o tráfego dos Spokes passe pelo Azure Firewall do Hub — sem ele, o roteamento forçado via UDR não funciona. O par Hub EUS ↔ Spoke EUS segue o mesmo padrão e está completo no repositório.
Passo 5 — Outputs
Os outputs do outputs.tf exportam os IDs e nomes de todos os recursos da hub-spoke landing zone com Terraform. Os próximos módulos da série — Firewall (Art. 03), Bastion (Art. 04), ASR (Art. 07) — referenciam esses valores via data "azurerm_*", sem precisar de remote state compartilhado:
output "rg_network_primary" {
description = "Resource Group de rede — região primária"
value = azurerm_resource_group.network_primary.name
}
output "hub_primary_id" {
description = "ID do Hub VNet primário"
value = azurerm_virtual_network.hub_primary.id
}
output "hub_primary_name" {
description = "Nome do Hub VNet primário"
value = azurerm_virtual_network.hub_primary.name
}
output "spoke_primary_subnet_aks_id" {
description = "ID da snet-aks primária"
value = azurerm_subnet.spoke_primary_aks.id
}
output "hub_primary_subnet_firewall_id" {
description = "ID da AzureFirewallSubnet primária"
value = azurerm_subnet.hub_primary_firewall.id
}
output "hub_primary_subnet_bastion_id" {
description = "ID da AzureBastionSubnet primária"
value = azurerm_subnet.hub_primary_bastion.id
}
Passo 6 — Executar o Terraform
Com todos os arquivos da hub-spoke landing zone com Terraform prontos, execute os comandos abaixo a partir da pasta terraform/. O plan salva o plano em arquivo binário, garantindo que o apply execute exatamente o que foi revisado e aprovado — sem surpresas por mudanças de estado paralelas:
cd art-02-hub-spoke-terraform/terraform/
# Exportar a subscription
export TF_VAR_subscription_id="<SUBSCRIPTION_ID>"
# Inicializar o provider e baixar plugins
terraform init
# Gerar e revisar o plano de execução
terraform plan -out=tfplan
# Aplicar o plano aprovado
terraform apply tfplan
O terraform apply cria 30 recursos: 4 Resource Groups, 4 VNets, 16 subnets e 6 peerings. Em condições normais, a execução completa em menos de 3 minutos — VNets são recursos rápidos de provisionar no Azure, ao contrário de AKS ou Firewall que podem levar 10 a 20 minutos.
Verificar a configuração
Após o apply, use os comandos abaixo para confirmar que a hub-spoke landing zone com Terraform foi provisionada corretamente. O status de todos os peerings deve ser Connected:
# Listar VNets criadas
az network vnet list \
--query "[?contains(name,'drlab')].{nome:name, rg:resourceGroup, espaco:addressSpace[0]}" \
-o table
# Verificar status dos peerings do Hub primário
az network vnet peering list \
--vnet-name vnet-hub-drlab-brazilsouth \
--resource-group rg-drlab-network-brazilsouth \
--query "[].{nome:name, estado:peeringState}" \
-o table
# Ver todos os outputs do módulo
terraform output
O resultado do terraform output deve retornar algo semelhante ao exemplo abaixo. Os valores reais variam conforme o projeto definido na variável project:
hub_primary_name = "vnet-hub-drlab-brazilsouth"
hub_secondary_name = "vnet-hub-drlab-eastus"
spoke_primary_name = "vnet-spoke-drlab-brazilsouth"
spoke_secondary_name = "vnet-spoke-drlab-eastus"
rg_network_primary = "rg-drlab-network-brazilsouth"
rg_workload_primary = "rg-drlab-workload-brazilsouth"
hub_primary_subnet_firewall_id = "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/..."
Troubleshooting
Os erros mais comuns ao provisionar uma hub-spoke landing zone com Terraform no Azure estão listados abaixo com as respectivas causas e soluções:
| Erro | Causa | Solução |
|---|---|---|
AddressSpaceNotAvailable |
Sobreposição de address spaces entre VNets | Confirmar que os quatro /16 são distintos: 10.0, 10.1, 10.2, 10.3 |
PeeringIsNotInSucceededState |
Peering criado mas a VNet remota ainda não respondeu | Aguardar 1-2 minutos e executar terraform apply novamente |
InvalidSubscriptionId |
TF_VAR_subscription_id não exportada ou com valor incorreto |
Verificar com echo $TF_VAR_subscription_id e az account show |
SubnetNameReserved |
Nome de subnet diferente do exigido pelo Azure | Usar exatamente: AzureFirewallSubnet, AzureBastionSubnet, GatewaySubnet |
AuthorizationFailed |
Usuário ou Service Principal sem permissão de Contributor | Atribuir role Contributor na subscription: az role assignment create --role Contributor |
Limpeza dos recursos
Para destruir todos os 30 recursos desta hub-spoke landing zone com Terraform e evitar cobranças desnecessárias, execute o terraform destroy. VNets e Resource Groups vazios têm custo zero no Azure, mas manter o ambiente limpo facilita a rastreabilidade dos recursos de cada artigo:
cd art-02-hub-spoke-terraform/terraform/
export TF_VAR_subscription_id="<SUBSCRIPTION_ID>"
terraform destroy -auto-approve
Consequentemente, ao chegar no Art. 03 — Azure Firewall e NSGs — você vai re-executar o terraform apply deste módulo para recriar a base de rede antes de adicionar os recursos de segurança. Por isso, mantenha os arquivos Terraform intactos após o destroy.
Próximos passos
Com a hub-spoke landing zone com Terraform provisionada, você tem a fundação de rede pronta para receber os serviços de segurança. No próximo artigo da série, vamos adicionar o Azure Firewall em cada Hub com regras de rede por camada, criar as User-Defined Routes que forçam o tráfego dos Spokes pelo Firewall, e configurar Network Security Groups segmentados por tipo de workload.
- 📖 Art. 01: Planejamento de DR Azure e Landing Zone
- ⚙️ Art. 02 (este): Hub-Spoke Landing Zone com Terraform
- 🔒 Art. 03: Azure Firewall e NSGs na Landing Zone
- 🔒 Art. 04: Azure Bastion na Landing Zone sem IP Público
- 🔒 Art. 05: Azure Front Door e Traffic Manager para Failover
- 🔒 Art. 06: DNS Privado Multi-Região no Azure
- 🔒 Art. 07: Azure Site Recovery para VMs Multi-Região
- 🔒 Art. 08: Azure Storage com Geo-Replicação para DR
- 🔒 Art. 09: AKS Multi-Região com Failover no Azure
- 🔒 Art. 10: Velero no AKS para Backup e Restore Cross-Region
- 🔒 Art. 11: Runbooks de Failover no Azure Automation
- 🔒 Art. 12: Simular um DR no Azure sem Impacto em Produção
- 🔒 Art. 13: Monitoramento do DR no Azure com Azure Monitor
Interessado em saber mais sobre artigos relacionados ao Microsoft Azure CLIQUE AQUI
🚀 Vamos nos conectar?
Não perca nenhuma oportunidade! Cadastre-se nas minhas redes e no canal do YouTube para receber conteúdos de TI, Cloud, Azure, Kubernetes e DevOps em primeira mão.
Dica: No Facebook, todos os artigos do blog são publicados automaticamente. Vale a pena curtir!
💬 Dúvidas ou Problemas?
Com o intuito de ajudar a comunidade, caso você tenha dúvidas ou encontre problemas na execução dos comandos deste artigo, deixe um comentário abaixo. Responderei o mais breve possível!
Muito obrigado pela visita e até o próximo post!
Jefferson Castilho Especialista em Cloud & DevOps.Este guia técnico é exclusivo do Blog do Castilho. Explore nossa para mais conteúdos sobre IA e Cloud.


