Hub-Spoke Landing Zone com Terraform

Hub-Spoke Landing Zone com Terraform

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.

Série: DR Azure e Landing Zone
  • 📖 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

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

NomeRegiãoFinalidade
rg-drlab-network-brazilsouthBrazil SouthHub VNet primário e subnets
rg-drlab-network-eastusEast USHub VNet secundário e subnets
rg-drlab-workload-brazilsouthBrazil SouthSpoke VNet primário e workloads
rg-drlab-workload-eastusEast USSpoke VNet secundário e workloads

VNets e Address Spaces

NomeTipoAddress SpaceRegião
vnet-hub-drlab-brazilsouthHub10.0.0.0/16Brazil South
vnet-hub-drlab-eastusHub10.1.0.0/16East US
vnet-spoke-drlab-brazilsouthSpoke10.2.0.0/16Brazil South
vnet-spoke-drlab-eastusSpoke10.3.0.0/16East 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:

SubnetCIDR (Brazil South)CIDR (East US)Observação
AzureFirewallSubnet10.0.0.0/2610.1.0.0/26Nome obrigatório, mín. /26
GatewaySubnet10.0.0.64/2710.1.0.64/27Nome obrigatório para VPN/ExpressRoute
AzureBastionSubnet10.0.0.128/2610.1.0.128/26Nome obrigatório, mín. /26 (Standard)
snet-management10.0.0.192/2710.1.0.192/27Jump servers de gerência

Subnets do Spoke (por região)

SubnetCIDR (Brazil South)CIDR (East US)Finalidade
snet-frontend10.2.1.0/2410.3.1.0/24Balanceadores e APIs
snet-backend10.2.2.0/2410.3.2.0/24Microsserviços e lógica de negócio
snet-data10.2.3.0/2410.3.3.0/24Bancos de dados e storage
snet-aks10.2.4.0/2210.3.4.0/22Nó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:

PeeringDireçãoTipo
peer-hub-primary-to-hub-secondaryBRS → EUSGlobal VNet Peering
peer-hub-secondary-to-hub-primaryEUS → BRSGlobal VNet Peering
peer-hub-primary-to-spoke-primaryHub BRS → Spoke BRSRegional
peer-spoke-primary-to-hub-primarySpoke BRS → Hub BRSRegional
peer-hub-secondary-to-spoke-secondaryHub EUS → Spoke EUSRegional
peer-spoke-secondary-to-hub-secondarySpoke EUS → Hub EUSRegional

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:

ErroCausaSoluçã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.

Série: DR Azure e Landing Zone
  • 📖 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.

 

Deixe uma resposta

Rolar para cima

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continue reading