Learn Terraform for Azure from scratch: a working path, not a syntax tour
Most Terraform tutorials are syntax tours: here is a block, here is a variable, good luck. This is a build path instead — one mental model, one real deployment, remote state before you need it, a pipeline at the end, and an honest accounting of the parts the tutorials skip that jobs actually test.
New to cloud? CAMPUX is a free, build-first course. Start here →
You can learn Terraform for Azure by building in a fixed order: understand state first, install the azurerm provider, deploy a resource group and storage account, move state to Azure Storage with locking, and wire plan and apply into GitHub Actions with OIDC — the honest catch is that tutorials stop before the parts jobs test.
That order matters more than any single piece of syntax. I have watched people learn HCL in an afternoon and then flounder for weeks, because they memorised resource blocks without understanding what Terraform does under them. So we start with the idea everything else hangs on.
The mental model: three things and a file
Terraform is a desired-state tool. You write down, in .tf files, what you want to exist — a resource group here, a storage account there, these tags on everything. Then Terraform compares that description against what actually exists in Azure, works out the difference, and shows it to you as a plan: create this, change that, destroy nothing. If you like the plan, you run apply and Terraform makes reality match the files. That is the whole loop. Write, plan, apply, repeat.
The piece beginners skip — and the piece that decides whether you ever get past toy examples — is state. Terraform keeps a file, terraform.tfstate, that records every resource it manages: the mapping between your code and the real objects in Azure. Without it, Terraform has no memory. It could not know that the azurerm_resource_group in your file is the same resource group it created yesterday, so it could not compute a diff at all.
Which is why losing state hurts so much. If the file is deleted or corrupted, Terraform forgets everything it built. Run apply again and it will try to create resources that already exist — and fail, because the names are taken — while the originals become orphans Terraform can no longer see, change, or destroy. Rebuilding a lost state file by hand is one of the most miserable afternoons in this line of work. I have had that afternoon. Every design decision that seems bureaucratic later — remote backends, locking, never editing state by hand — exists to prevent it.
Plan and apply are the verbs. State is the memory. Almost every Terraform disaster you will ever see is a state problem wearing a disguise.
Setup: the provider and a login
You need three things installed: the Terraform CLI, the Azure CLI, and an Azure subscription (the free tier is fine for everything in this article). For local learning, authentication is one command — az login — and Terraform's Azure provider will ride on that session. No service principals, no secrets in files. That comes later, in the pipeline, and it is done properly there.
Make an empty folder, and create main.tf with the provider configuration:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = "<your-subscription-id>"
}
Two notes. The azurerm provider is the plugin that translates your HCL into Azure API calls; the version constraint pins you to the 4.x line so an upgrade never surprises you mid-project. And the empty features {} block is required — it looks pointless; it is just how the provider is configured. Run terraform init in the folder and Terraform downloads the provider. You are set up.
The first real build: a resource group, a storage account, tags
Add this below the provider block:
resource "azurerm_resource_group" "lab" {
name = "rg-terraform-lab"
location = "eastus2"
tags = {
environment = "lab"
managed_by = "terraform"
}
}
resource "azurerm_storage_account" "lab" {
name = "sttflab<something-unique>"
resource_group_name = azurerm_resource_group.lab.name
location = azurerm_resource_group.lab.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = azurerm_resource_group.lab.tags
}
Notice what the storage account does: instead of repeating the resource group's name and location, it references them — azurerm_resource_group.lab.name. That reference is also how Terraform learns the ordering: the group must exist before the account that lives in it. You never write "do this first"; the dependency graph comes from the references. (Storage account names are globally unique across all of Azure, lowercase letters and numbers only, hence the unique suffix.)
Now the loop, and this is the part to slow down on. Run terraform plan and read the output line by line. Every resource is prefixed with a symbol: + create, ~ change in place, -/+ destroy and recreate, - destroy. At the bottom, a summary: Plan: 2 to add, 0 to change, 0 to destroy. Beginners skim this. Working engineers read it the way a pilot reads a checklist, because the plan is the only thing standing between you and a change you did not intend. Building the habit now, on a storage account that costs pennies, is what saves you later when the same -/+ symbol sits next to a production database.
Run terraform apply, type yes, and go look in the portal. Your resources are there, and you did not click anything to make them.
Then do the thing tutorials never make you do: change something and watch the diff. Add a tag, run plan, and see a ~ update-in-place. Change the storage account name, run plan, and see -/+ — Terraform must destroy and recreate, because Azure will not rename a storage account. That difference between "change in place" and "replace" is the most valuable instinct this stage can teach. Some changes are free; some are destructive; the plan always tells you which, if you read it.
Remote state: the day a second person shows up
Everything so far kept state in a local file next to your code. That works right up until the day a second person — or a pipeline, which is just a very tireless second person — needs to run Terraform against the same infrastructure. Now there are two laptops, each with its own idea of the truth, and whichever applies second is diffing against a stale memory. Local state also means your infrastructure's only record lives on a machine that can be stolen, wiped, or dropped in a lake.
The fix on Azure is pleasantly self-referential: store the state file in an Azure Storage account. Create a small, separate resource group and storage account for it (by hand or with the CLI — this one thing lives outside Terraform, since Terraform cannot store its state in a bucket it has not created yet), then add a backend block:
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstate<something-unique>"
container_name = "tfstate"
key = "lab.terraform.tfstate"
}
}
Run terraform init again and Terraform offers to migrate your existing local state into the blob. Say yes. From now on, every plan and apply reads and writes the shared copy — and, quietly, you got the other thing teams need: locking. The azurerm backend takes a lease on the state blob during operations, so two people (or a person and a pipeline) cannot apply at the same time and corrupt the file. You do not configure it; it is just how the backend works. Turn on blob versioning too, so a bad write is a restore, not a catastrophe. The full pattern, including recovering a broken lease, is in Terraform state on Azure.
Variables, outputs — and permission to stay flat
Hard-coded values stop scaling the moment you want the same infrastructure twice — a dev copy and a prod copy, say. Variables fix that:
variable "location" {
type = string
default = "eastus2"
description = "Azure region for all resources"
}
output "storage_account_name" {
value = azurerm_storage_account.lab.name
}
Reference the variable as var.location, override it per environment with a .tfvars file, and use outputs to surface the values other things need — a connection string, a name, an ID — without going spelunking in the portal.
Now the advice you will not find in most guides: do not write modules yet. Beginners over-modularize, reliably, and I think it is because every "best practices" post shows a modules directory and nobody wants to look junior. So they wrap a single storage account in a module with eleven input variables, and now every change means threading a new variable through an interface that exists for no reason. A module is a function, and you do not extract a function from code you have written once. Write flat .tf files until you feel real repetition — the third time you paste the same four-resource cluster and change two values, that is the signal. Extracting a module from working, repeated code takes twenty minutes. Untangling a premature abstraction takes a weekend.
CI: plan and apply from GitHub Actions, no stored secrets
Running Terraform from your laptop is fine for learning and wrong for teams: applies should come from one controlled place, with a reviewable log, off a reviewed branch. The modern shape of that on Azure is GitHub Actions authenticating with OIDC — the workflow proves its identity to Entra ID with a short-lived token, and no client secret sits in repository settings for anyone to leak. If a course teaches you to store a service principal secret in CI, it is teaching a habit real teams are actively removing; the reasoning is in pipelines without secrets, and the hands-on setup — app registration, federated credential, role assignment — is Class 23.
The pipeline itself follows a rhythm you already know, because it is your local loop with a pull request in the middle:
permissions:
id-token: write # OIDC token for Azure
contents: read
steps:
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: terraform init
- run: terraform plan
Those three IDs are identifiers, not secrets — the trust lives in the federated credential, which only honours tokens from your specific repo and branch. The working pattern: pull requests run terraform plan and post the diff for review; merging to main runs terraform apply. The plan on the PR is the same read-it-line-by-line artifact you practised earlier, which is exactly why I made you practise it.
The Bicep question, answered once
Every Terraform-on-Azure learner hits this fork, so here is the honest version without re-litigating the whole debate. Bicep is Microsoft's own language for Azure: Azure-only, and stateless by design — the Azure Resource Manager itself is the source of truth, so there is no state file to store, lock, or lose. If your world is pure Azure and will stay that way, Bicep is a legitimate choice, and anyone who tells you otherwise is selling something. Terraform is multicloud with explicit state, which costs you the state management you just learned and buys you one language across Azure, AWS, GCP, GitHub, Kubernetes, and a thousand other providers.
The market, as of 2026, rewards Terraform: it appears in far more job postings, and it is the tool a mixed-cloud employer will assume you know. That is a hiring observation, not a quality verdict — treat the direction as settled and the exact ratio as moving. My actual advice is to stop treating it as either/or: the concepts transfer almost entirely, and knowing both is a genuine edge on Azure teams. The longer comparisons are in Bicep vs Terraform and — for the adjacent "what about Ansible?" question — Ansible vs Terraform. The free bootcamp teaches both back to back: Class 20 covers Bicep, Class 21 covers Terraform, and doing them in that order makes each one sharper.
The honest gap: tutorial Terraform vs production Terraform
Here is the section the tutorials structurally cannot write, because it is about where they stop. Everything above — greenfield resources, clean state, one author — is tutorial Terraform, and it is real and necessary. But production Terraform is mostly about the mess that accumulates around that clean core, and the job interviews I have sat on both sides of probe the mess, not the syntax. Four gaps, and what to practise for each:
- State migrations. Sooner or later you split one state file into several, rename resources, or move a resource between configurations — and each is a state surgery, not a code edit. Practise: rename a resource in your lab code, watch plan threaten to destroy and recreate it, then use a
movedblock (orterraform state mv) to fix it with zero changes. That one exercise teaches more than a week of reading. - Drift. Someone will change a tag, a firewall rule, a SKU in the portal, and now reality disagrees with both your code and your state. Practise: deliberately edit your lab storage account in the portal, run
terraform plan, and study how the drift surfaces. Then decide, like a real team must: adopt the change into code, or let apply revert it? - Imports. Real companies have years of click-created resources that Terraform has never heard of, and bringing them under management is patient, resource-by-resource work. Practise: create a resource group by hand in the portal, then bring it under Terraform with an
importblock — write the code, import, and iterate until plan shows no changes. That "no changes" moment is the skill. - Module and provider versioning. In production, an unpinned provider version is a time bomb: a major upgrade lands on a Tuesday and forty plans go red. Practise: pin your provider, commit the lock file, then deliberately bump the constraint and read the upgrade guide like it is your job — because one day it will be.
None of this is glamorous, which is exactly why it is where the jobs live. A candidate who can talk through recovering from drift or importing a legacy resource group reads as someone who has operated Terraform rather than someone who finished a course about it.
The path, week by week
If you want the sequence above as a calendar, here is the honest version. Faster is possible if you are full-time; slower is fine and normal.
| Week | You build | The concept that actually lands |
|---|---|---|
| 1 | Provider setup, resource group + storage account, the plan/apply loop | Desired state, and reading a plan line by line |
| 2 | Change things on purpose: tags, names, SKUs; destroy and rebuild | Update-in-place vs destroy-and-recreate |
| 3 | Remote state in Azure Storage, migrate local state, versioning on | State is shared memory; locking exists for a reason |
| 4 | Variables, tfvars per environment, outputs; still flat files | Parameterise before you abstract |
| 5 | GitHub Actions: plan on PR, apply on merge, OIDC login | The pipeline is just your loop with review in the middle |
| 6 | The mess, on purpose: a rename with moved, portal drift, an import | Production Terraform is state operations, not syntax |
Where to take it: a destination project
Learning stalls without a destination, so give yourself one that looks like work. The Terraform landing zone lab is the one I point people at after week six: networking, RBAC, policy, and structure — the shape of what platform teams actually maintain, small enough to finish. It lives with the other CLI-and-IaC labs in the public campux-labs repo, all free, all designed to leave you with a repo you can show a hiring manager. And if you want this path with an instructor in the room when your state file does something weird, the live cohort builds it as a group — but the self-paced material above is complete on its own, and free is the right price to start at.
It worked for them.
Questions people also ask
How long does it take to learn Terraform for Azure?
Most people can write and apply their first real Terraform configuration against Azure in a weekend, and reach basic working competence — remote state, variables, a small CI pipeline — in four to six weeks of steady practice. Production comfort takes longer, because it depends on things tutorials rarely cover: state migrations, drift, and importing resources that were created by hand. If you already know Azure, the timeline shortens; Terraform syntax is the easy part, and knowing what the resources should look like is the hard part.
Should I learn Terraform or Bicep for Azure?
If your work is pure Azure and will stay that way, Bicep is a legitimate choice — it is Azure-only by design and you never manage a state file. Terraform is the better career bet for most people because it works across clouds and shows up in far more job postings. The concepts transfer either way: providers, plans, and declarative resources look similar in both. Learning Terraform first does not lock you out of Bicep later, and the reverse is also true.
Do I need to know Azure before learning Terraform?
You need some Azure, but not much. Terraform is a way of describing Azure resources in code, so you should already know what a resource group, a storage account, and a virtual network are before you describe them. You do not need certifications or years of portal experience. A reasonable approach is to learn the handful of core services first, then let Terraform be the way you practise them — writing a resource in HCL teaches you its settings faster than clicking through the portal does.
What is the Terraform state file and why does it matter?
The state file is Terraform's record of every resource it manages — the mapping between your code and the real objects in Azure. When you run plan, Terraform compares your code against state and reality to work out what to change. If the state file is lost or corrupted, Terraform forgets what it built: it may try to recreate resources that already exist, or leave orphans it can no longer see. That is why teams store state remotely in Azure Storage with locking rather than on a laptop.
Is Terraform free to use with Azure?
The Terraform CLI is free to download and use under HashiCorp's Business Source License, and the azurerm provider is free as well. You pay only for the Azure resources you create, so practising with a resource group, a storage account, and tags costs a few cents at most if you destroy resources when you finish. HCP Terraform, the hosted service, has a free tier and paid plans, but you do not need it to learn — local runs plus a state file in Azure Storage cover everything in this guide.