Last reviewed: May 2026
Build the AWS services on the AZ-104 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-104 reference workload — a Resource Group, a VNet with an NSG-protected private subnet, a Linux VM with a system-assigned managed identity, a Storage Account the VM can read from via that identity, and a Log Analytics workspace receiving VM diagnostics. Every block ties to one of the five AZ-104 exam domains.
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).~/.ssh/id_rsa.pub (or change the path in Step 4) for the VM's admin login.The VM is the bill:
Stop the VM (az vm deallocate) when not actively using it to halve the compute cost. Destroy the whole RG to fully stop billing.
Standard Azure opener: pin azurerm ~> 4.0, create the Resource Group, read the local SSH public key as a data source so the VM in Step 4 can use it. AZ-104's Identity and Governance domain tests Resource Groups as the smallest RBAC scope; tags here cascade for cost-tracking under Monitor Azure resources.
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "azurerm" {
features {}
}
resource "random_id" "suffix" {
byte_length = 2
}
locals {
tags = {
Project = "certlabpro-az-104"
ManagedBy = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "certlabpro-az-104-rg"
location = "eastus"
tags = local.tags
}Every AZ-104 workload runs inside a Virtual Network. We create a /16 VNet and carve out one /24 subnet for the VM. The NSG attached to the subnet allows SSH (port 22) inbound — production would restrict the source CIDR; the lab opens it to * so you can connect from any IP.
The AZ-104 Deploy and Manage Networking domain tests this layered-security pattern repeatedly: NSGs are stateful, they apply to subnets or NICs (we apply to subnet here — the AZ-104 best-practice answer), and the default-deny + explicit-allow rule structure is how Azure NSGs differ from AWS security groups (Azure NSGs have explicit rule numbers; AWS SGs don't).
resource "azurerm_virtual_network" "main" {
name = "certlabpro-az-104-vnet"
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" "app" {
name = "app"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_security_group" "app" {
name = "certlabpro-az-104-nsg"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
security_rule {
name = "AllowSSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = local.tags
}
resource "azurerm_subnet_network_security_group_association" "app" {
subnet_id = azurerm_subnet.app.id
network_security_group_id = azurerm_network_security_group.app.id
}Standard AZ-104 storage defaults: Standard tier, LRS, HTTPS-only, TLS 1.2 minimum, public access blocked. We'll grant the VM's managed identity read access to this account in Step 4. The Manage Azure Identities and Governance domain tests this managed-identity → resource pattern as the password-less alternative to storing storage account keys in app config.
resource "azurerm_storage_account" "data" {
name = "az104data${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
tags = local.tags
}The VM is the canonical AZ-104 compute artifact. We use Standard_B1s (the smallest free-tier-eligible Linux SKU), Ubuntu 22.04 LTS, SSH-key auth (the AZ-104 password-anti-pattern answer), and a system-assigned managed identity.
The role assignment grants that managed identity Storage Blob Data Reader on the storage account from Step 3 — meaning the VM can call the storage REST APIs using its identity token, with no keys stored on disk. This is the AZ-104 Implement and Manage Storage + Identity crossover question: how does a VM access storage without credentials? Managed identity + RBAC role assignment.
resource "azurerm_public_ip" "vm" {
name = "certlabpro-az-104-vm-pip"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
allocation_method = "Static"
sku = "Standard"
tags = local.tags
}
resource "azurerm_network_interface" "vm" {
name = "certlabpro-az-104-vm-nic"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
ip_configuration {
name = "ipconfig1"
subnet_id = azurerm_subnet.app.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm.id
}
tags = local.tags
}
resource "azurerm_linux_virtual_machine" "main" {
name = "certlabpro-az-104-vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B1s"
admin_username = "azureuser"
disable_password_authentication = true
network_interface_ids = [azurerm_network_interface.vm.id]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
identity {
type = "SystemAssigned"
}
tags = local.tags
}
resource "azurerm_role_assignment" "vm_storage_reader" {
scope = azurerm_storage_account.data.id
role_definition_name = "Storage Blob Data Reader"
principal_id = azurerm_linux_virtual_machine.main.identity[0].principal_id
}AZ-104's Monitor and Maintain Azure Resources domain (~10–15% of the exam) leans on Log Analytics as the destination for everything observable. We create the workspace and a Diagnostic Setting on the storage account from Step 3 — every storage transaction now lands in queryable KQL form.
For the VM we'd typically install the Azure Monitor Agent (AMA) via a VM extension — that's the modern post-Log-Analytics-Agent path. For the lab we set up the workspace; installing the agent itself is a separate runtime step (azurerm_virtual_machine_extension) that's covered in Browse / Editorial. The architectural shape — workspace exists, resources stream into it — is what AZ-104 questions test.
resource "azurerm_log_analytics_workspace" "main" {
name = "certlabpro-az-104-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" "storage" {
name = "diag"
target_resource_id = "${azurerm_storage_account.data.id}/blobServices/default/"
log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
enabled_log {
category = "StorageRead"
}
enabled_log {
category = "StorageWrite"
}
metric {
category = "AllMetrics"
enabled = true
}
}terraform destroy tears down everything. Two notes:
az vm deallocate stops compute billing (storage still bills). terraform destroy removes everything.Static allocation — it survives VM deallocation, holds the IP address reservation. Destroy or convert to Dynamic if you don't want to pay for an unused public IP (~$0.005/hour).AZ-104 covers a broad ops surface — Azure Backup, Recovery Services Vault, Site Recovery, Azure Files / Azure File Sync, Azure Migrate, Application Gateway, Load Balancer, Azure Bastion, VPN Gateway, ExpressRoute, Azure Firewall, Azure Policy, Azure Blueprints (deprecating), Conditional Access in Entra ID, B2B/B2C, RBAC custom roles, and ARM/Bicep templates.
We stick to the VNet + VM + Storage + Monitor baseline because it's the substrate every other AZ-104 question assumes. Add an Application Gateway for layer-7 routing; add a VPN Gateway for hybrid connectivity; add Recovery Services Vault for backup — each is a focused bolt-on to this base.
For service-by-service coverage, see the Browse, Playbook, and Editorial sections of this cert page.