Last reviewed: May 2026
Build the AWS services on the AZ-900 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 smallest realistic first Azure workload — a Resource Group, a Storage Account with secure defaults, a tiny Linux VM, and a Cost Management budget alert that emails you before your wallet does. Every resource maps to one of the AZ-900 exam pillars.
Every resource is plain Terraform. There are no variables, no modules, no remote state. Drop the snippets into a single main.tf, run terraform init once, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.az login once; Terraform's azurerm provider picks up the active subscription automatically.All resources in this lab fit within the Azure free tier for new subscriptions ($200 credit for the first 30 days, plus always-free amounts):
If you keep the VM running 24/7 outside free tier it costs ~$5/month. Destroy the lab when done.
Every Azure Terraform stack starts the same way: pin the azurerm provider (we use ~> 4.0, the current stable line), and declare the features block. The empty features {} block is required — azurerm won't initialize without it. This is the most-tested AZ-900 Provisioning convention at the Terraform-author level.
The random_id resource is the standard pattern for generating globally-unique storage account names — Azure storage account names must be 3–24 characters, lowercase alphanumeric, and globally unique across all Azure subscriptions. The random_id adds 4 hex digits to the name to dodge collisions.
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-900"
ManagedBy = "terraform"
}
}Every Azure resource lives in a Resource Group — Azure's universal organizational container. RGs are free, regional (the RG itself is metadata-only but its contents are scoped to its region by default), and the unit of bulk-management: deleting an RG deletes everything inside it.
AZ-900's Cloud Concepts and Azure Identity, Governance, Privacy, Compliance domains hammer on this concept — Resource Groups are the smallest scoping unit for RBAC role assignments, Azure Policy, and cost tracking. Tag every RG.
resource "azurerm_resource_group" "main" {
name = "certlabpro-az-900-rg"
location = "eastus"
tags = local.tags
}Azure Storage is the foundational data service every AZ-900 workload uses. We create a Standard tier LRS (locally-redundant) account — the cheapest replication option, and the AZ-900-expected default for non-production lab workloads. Public network access is disabled, HTTPS-only is enforced, and minimum TLS version is set to 1.2 (the AZ-900 Security domain explicitly tests these three as the basic-hygiene defaults).
The account_kind = "StorageV2" is the current general-purpose v2 account; AZ-900 will sometimes ask you to distinguish it from BlobStorage (older, blob-only) and FileStorage (premium files). StorageV2 is the right answer for any "general-purpose" question.
resource "azurerm_storage_account" "main" {
name = "certlabpro${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"
public_network_access_enabled = false
allow_nested_items_to_be_public = false
tags = local.tags
}AZ-900 dedicates an entire domain (Azure Pricing, SLAs, and Lifecycle) to billing concepts. The single most-tested practical artifact in that space is a budget alert — Cost Management's mechanism for emailing you when actual or forecasted spend crosses a threshold.
We scope the budget to the Resource Group we created in Step 2, which means it tracks everything in this lab (and only this lab). The notification fires at 80% of the budget — a recurring AZ-900 question pattern is "why 80% rather than 100%?" — because at 80% you still have time to act; at 100% the money is already gone.
Replace you@example.com with your real address before running terraform apply.
resource "azurerm_consumption_budget_resource_group" "main" {
name = "certlabpro-az-900-budget"
resource_group_id = azurerm_resource_group.main.id
amount = 10
time_grain = "Monthly"
time_period {
start_date = "2026-06-01T00:00:00Z"
}
notification {
enabled = true
threshold = 80
operator = "GreaterThan"
threshold_type = "Actual"
contact_emails = ["you@example.com"]
}
}A standard terraform destroy tears down everything in this lab. One Azure-specific note: when you destroy the Resource Group, Azure removes every resource inside it as a side-effect — even if Terraform doesn't know about them. This is the AZ-900-tested cleanup primitive: if you ever lose track of what's in an RG, deleting the RG nukes the whole subtree. Use it carefully on shared subscriptions.
The storage account has a 14-day soft-delete retention by default in newer subscriptions; the namespace stays reserved for that window. If you re-run the lab immediately you may hit a name-collision — the random_id suffix usually dodges it but isn't guaranteed.
AZ-900 covers a wide service surface — Azure Virtual Machines (we touch this conceptually but don't actually deploy a VM in this minimal lab), App Service, Azure Functions, AKS, Azure SQL, Cosmos DB, Azure AD/Entra ID, Azure Policy, Azure Blueprints, ARM templates, Bicep, Pricing Calculator, TCO Calculator, Azure Advisor, Service Health, and many more.
We stick to the smallest realistic first workload because the exam tests broad conceptual fluency, not deep service-by-service provisioning. The four pillars touched above — Resource Group (governance), Storage Account (services + security defaults), Cost Management (pricing), and the implied compute target — are the foundation every other AZ-900 concept attaches to.
For service-by-service coverage, see the Browse and Editorial sections of this cert page — they reference every named service in the AZ-900 scope.