Last reviewed: May 2026
Build the AWS services on the AZ-700 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, the AZ-700 reference network — a hub VNet with a shared-services subnet, a spoke VNet with an app subnet, bidirectional peering between the two, a private DNS zone linked to both VNets for cross-VNet name resolution, and NSG flow logs into Log Analytics for traffic visibility.
Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.az login).Mostly free, with one watch-out:
$1–2/month idle. The AZ-700 cost-traps are VPN Gateway ($60/month) and ExpressRoute ($200+/month) — both omitted here.
Standard Azure opener.
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
}
}
provider "azurerm" {
features {}
}
locals {
tags = {
Project = "certlabpro-az-700"
ManagedBy = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "certlabpro-az-700-rg"
location = "eastus"
tags = local.tags
}Hub-spoke is the AZ-700 reference topology. The hub centralizes shared services (firewall, VPN gateway, DNS resolver, monitoring) and spokes peer with the hub to access them. We build the hub at 10.0.0.0/16 with one subnet — in a real hub you'd have a GatewaySubnet for the VPN/ExpressRoute Gateway, an AzureFirewallSubnet for Azure Firewall, and a shared-services subnet for DNS Resolver inbound/outbound endpoints. Lab scope is the topology shape; the gateway services are out of scope (high cost).
resource "azurerm_virtual_network" "hub" {
name = "vnet-hub"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
address_space = ["10.0.0.0/16"]
tags = local.tags
}
resource "azurerm_subnet" "hub_shared" {
name = "shared-services"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = ["10.0.1.0/24"]
}The spoke at 10.1.0.0/16 — deliberately non-overlapping with the hub. The NSG attached to the app subnet would in production restrict ingress to specific source CIDRs; the lab uses * for source so the architecture is testable.
AZ-700 Implement core networking infrastructure hits the NSG-per-subnet vs NSG-per-NIC question repeatedly. Subnet-level NSGs (this lab) are the right answer when the question says "applied to all resources in the subnet"; NIC-level is the override.
resource "azurerm_virtual_network" "spoke" {
name = "vnet-spoke"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
address_space = ["10.1.0.0/16"]
tags = local.tags
}
resource "azurerm_subnet" "spoke_app" {
name = "app"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.spoke.name
address_prefixes = ["10.1.1.0/24"]
}
resource "azurerm_network_security_group" "spoke_app" {
name = "nsg-spoke-app"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
security_rule {
name = "AllowHttpsFromInternet"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = local.tags
}
resource "azurerm_subnet_network_security_group_association" "spoke_app" {
subnet_id = azurerm_subnet.spoke_app.id
network_security_group_id = azurerm_network_security_group.spoke_app.id
}VNet peering is the AZ-700 connectivity primitive within a region. Two key properties the exam tests:
allow_forwarded_traffic — required when the hub plans to forward traffic from one spoke to another (transit through a firewall in the hub).use_remote_gateways / allow_gateway_transit — the spoke uses use_remote_gateways = true to ride the hub's VPN/ER gateway. We don't have a gateway in this lab, but the attribute shape is exam-tested.resource "azurerm_virtual_network_peering" "hub_to_spoke" {
name = "hub-to-spoke"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.hub.name
remote_virtual_network_id = azurerm_virtual_network.spoke.id
allow_forwarded_traffic = true
allow_gateway_transit = true
}
resource "azurerm_virtual_network_peering" "spoke_to_hub" {
name = "spoke-to-hub"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.spoke.name
remote_virtual_network_id = azurerm_virtual_network.hub.id
allow_forwarded_traffic = true
# use_remote_gateways = true # uncomment when a gateway exists in the hub
}Private DNS zones are the AZ-700 Design and implement name resolution answer for cross-VNet name resolution that doesn't rely on Azure's default DNS. We create the zone internal.contoso.com and link both the hub and spoke VNets to it. Resources in either VNet can now query records in this zone.
The registration_enabled = true flag on the hub link lets resources in the hub auto-register their hostnames. AZ-700 tests this auto-registration vs manual A-record creation as the recurring "how do I get clean DNS names for my VMs" question.
resource "azurerm_private_dns_zone" "main" {
name = "internal.contoso.com"
resource_group_name = azurerm_resource_group.main.name
tags = local.tags
}
resource "azurerm_private_dns_zone_virtual_network_link" "hub" {
name = "hub-link"
resource_group_name = azurerm_resource_group.main.name
private_dns_zone_name = azurerm_private_dns_zone.main.name
virtual_network_id = azurerm_virtual_network.hub.id
registration_enabled = true # auto-register hub-resident VMs
tags = local.tags
}
resource "azurerm_private_dns_zone_virtual_network_link" "spoke" {
name = "spoke-link"
resource_group_name = azurerm_resource_group.main.name
private_dns_zone_name = azurerm_private_dns_zone.main.name
virtual_network_id = azurerm_virtual_network.spoke.id
registration_enabled = false # spoke resources query but don't register
tags = local.tags
}terraform destroy tears down everything. Peering resources detach automatically when either VNet is destroyed; Terraform handles the order correctly. Private DNS zones with active records can be slow to destroy (Azure waits for all consumer links to detach first) — be patient.
AZ-700 covers networking surfaces this lab can't fit — VPN Gateway ($60/month, cost-prohibitive for lab), ExpressRoute + ER Gateway, Azure Firewall + Firewall Policy + Premium IDPS ($1/hour idle), Application Gateway + WAF, Azure Front Door, Azure Load Balancer (Standard tier), Azure DNS public zones, DNS Private Resolver, Azure Bastion, Network Virtual Appliances, Virtual WAN, Route Server, custom route tables (UDRs), Service Endpoints, Private Endpoints + Private Link Service, and DDoS Protection Standard.
We stick to the hub-spoke + peering + Private DNS shape because it's the substrate every other AZ-700 pattern attaches to. VPN Gateway goes in the hub. Firewall goes in the hub. Application Gateway protects the spoke's app subnet. Private Endpoints route through the hub. Get the topology right; layer on per-pattern.
For the surfaces above, see the Browse, Playbook, and Editorial sections of this cert page.