Last reviewed: May 2026
Build the AWS services on the AZ-305 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-305 reference architecture core — a hub-spoke VNet topology (one hub, one spoke, peered), an Azure Front Door for global HTTPS entry, a Key Vault for secrets and certificate management with RBAC authorization, and the diagnostic plumbing every Architect-Expert design assumes. Every block ties to an AZ-305 design domain.
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).Whole stack ~$36/month idle. Front Door is the cost trap — destroy promptly if you're not actively testing.
Standard Azure opener. AZ-305 expects you to reach for the eastus / westus pair for any "two-region" question — they're the canonical AZ-305 region pair for latency tests and paired-region failover scenarios.
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
random = { source = "hashicorp/random", version = "~> 3.6" }
}
}
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = true
}
}
}
resource "random_id" "suffix" {
byte_length = 3
}
data "azurerm_client_config" "current" {}
locals {
tags = {
Project = "certlabpro-az-305"
ManagedBy = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "certlabpro-az-305-rg"
location = "eastus"
tags = local.tags
}Hub-spoke is the AZ-305 reference network design: a central hub VNet hosts shared services (firewall, VPN gateway, DNS), and spoke VNets host workloads. Spokes peer with the hub bidirectionally; spoke-to-spoke traffic transits through the hub. The exam tests this topology endlessly because it's the foundation of any multi-workload Azure landing zone.
We build the simplest possible hub-spoke: one hub VNet (10.0.0.0/16), one spoke (10.1.0.0/16), bidirectional peering. Real hub-spoke designs add the Azure Firewall in the hub and next_hop_in_ip_address route tables forcing spoke traffic through it — that's covered conceptually but skipped here for cost (Azure Firewall is ~$1/hour idle).
resource "azurerm_virtual_network" "hub" {
name = "certlabpro-az-305-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"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_virtual_network" "spoke" {
name = "certlabpro-az-305-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_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
}
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
}Key Vault is the AZ-305 secrets-and-certs primitive. Two big architectural decisions the exam tests: RBAC vs Access Policies (RBAC is the modern default; Access Policies are legacy), and soft-delete + purge protection (soft-delete is on by default; purge protection makes the soft-delete window non-overridable — required for some compliance regimes).
We enable RBAC authorization and assign the current Terraform principal as Key Vault Administrator so subsequent secret/cert resources work. AZ-305 questions about "the application can't read its database password" 90% of the time end with "because the managed identity didn't have Key Vault Secrets User on the vault".
resource "azurerm_key_vault" "main" {
name = "kv-az305-${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
enable_rbac_authorization = true
soft_delete_retention_days = 7 # 7 minimum; 90 for purge protection in production
purge_protection_enabled = false # set true for production / compliance
public_network_access_enabled = true
tags = local.tags
}
resource "azurerm_role_assignment" "kv_admin_self" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Administrator"
principal_id = data.azurerm_client_config.current.object_id
}Front Door is the AZ-305 global load balancer + WAF + CDN answer. The exam tests it against three alternatives: Application Gateway (regional layer-7), Traffic Manager (DNS-based, no traffic transit), and Load Balancer (regional layer-4). Front Door is the right answer for any "globally distributed users + HTTPS + WAF + caching" scenario.
We provision a Front Door profile + endpoint + a stub origin group. In production you'd plug a Web App or AKS ingress as the origin; for the lab the topology shape is what AZ-305 questions test. The Standard tier supports the security and routing features the exam expects (WAF, custom rules, session affinity); Premium adds private-link origins for fully-private backend access.
resource "azurerm_cdn_frontdoor_profile" "main" {
name = "certlabpro-az-305-fd"
resource_group_name = azurerm_resource_group.main.name
sku_name = "Standard_AzureFrontDoor"
tags = local.tags
}
resource "azurerm_cdn_frontdoor_endpoint" "main" {
name = "certlabpro-az-305-${random_id.suffix.hex}"
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id
tags = local.tags
}
resource "azurerm_cdn_frontdoor_origin_group" "main" {
name = "app-origins"
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id
session_affinity_enabled = false
load_balancing {
sample_size = 4
successful_samples_required = 3
}
health_probe {
path = "/"
request_type = "HEAD"
protocol = "Https"
interval_in_seconds = 30
}
}AZ-305's Design data storage and integration domain plus Design business continuity both depend on having observability around the architecture pieces. We create a Log Analytics workspace and wire Front Door access logs + Key Vault audit logs into it.
With the diagnostics plumbing in place, the AZ-305 reference architecture is shaped: global ingress (Front Door) → hub-spoke network → workload spoke → secrets layer (Key Vault) → observability fabric (Log Analytics). Every additional architectural addition (Application Gateway behind Front Door, Cosmos DB in the spoke, Logic Apps in the hub) plugs into this base.
resource "azurerm_log_analytics_workspace" "main" {
name = "certlabpro-az-305-logs"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "PerGB2018"
retention_in_days = 30
tags = local.tags
}
resource "azurerm_monitor_diagnostic_setting" "key_vault" {
name = "diag"
target_resource_id = azurerm_key_vault.main.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
enabled_log {
category = "AuditEvent"
}
metric {
category = "AllMetrics"
enabled = true
}
}
resource "azurerm_monitor_diagnostic_setting" "front_door" {
name = "diag"
target_resource_id = azurerm_cdn_frontdoor_profile.main.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
enabled_log {
category = "FrontDoorAccessLog"
}
enabled_log {
category = "FrontDoorHealthProbeLog"
}
}terraform destroy tears down everything. The Front Door profile is the line item billing 24/7 (~$35/month). Destroy promptly if you're not exploring it. Key Vault has 7-day soft-delete here (configurable up to 90); we set purge_soft_delete_on_destroy = true in the provider so destroy actually purges the vault instead of leaving the namespace reserved.
AZ-305 covers an enormous architectural surface — Application Gateway, Azure Firewall, VPN Gateway + ExpressRoute, Private Link / Private Endpoints, Azure Active Directory B2B / B2C, Conditional Access, Privileged Identity Management, AKS at architectural scale, Service Fabric, Azure API Management, multi-region paired deployments, geo-replicated Cosmos / SQL / Storage, BCDR designs (Site Recovery), Cost Management at scale, Landing Zone designs, and Azure Policy initiatives.
We stick to the four foundational primitives because they're the substrate every AZ-305 multi-component design assumes. App Gateway sits in the hub spoke from Step 2. AKS deploys into the spoke VNet. Cosmos DB exposes private endpoints into the spoke. Defender for Cloud reads the Log Analytics workspace. Architecture is composition; this lab gives you the base shapes.
For coverage of the surfaces not provisioned, the Browse, Playbook, and Editorial sections of this cert page have conceptual material.