CAMPUX Cloud Bootcamp Phase Three · Class Twenty-One
Phase Three — DevOps Core
Reading 40 min · Drills 6 · 2 Labs
Aligned to AZ-400
Class Twenty-One

Infrastructure as code: Terraform

Last class the file was the truth and Azure kept the books; this class you meet a tool that keeps its own books — a ledger called state that sits between your code and the cloud, and that most beginners discover only by breaking it.

§1

Same job, different philosophy

Terraform does the same job as Bicep: you declare the infrastructure you want in text, and a tool makes reality match the file. The difference is philosophical, and it matters more than the syntax. Bicep hands your file to Azure Resource Manager and lets Azure work out what exists and what to change — Azure is the bookkeeper. Terraform trusts nobody's books but its own. It keeps a private record of every resource it has ever created — their real Azure ids, their last-known settings — in a file called state, and every decision it makes is a comparison between three things: what your code asks for, what its state remembers, and what the cloud actually contains.1

Terraform state
Terraform's own record of the resources it manages — the mapping from each resource block in your code to the real cloud resource it created, with its last-known attributes. Code says what you want; the cloud is what exists; state is how Terraform knows which is which.

Why carry that third thing at all? Because it buys Terraform its defining trait: it is not married to Azure. ARM can be the bookkeeper for Azure resources because ARM is Azure. Terraform manages Azure, AWS, GCP, GitHub, and a few thousand other platforms through plug-in providers, and no single cloud can keep books for all of them — so Terraform keeps its own. The price of that independence is that the books must be looked after. Lose the state file and Terraform forgets everything it built; corrupt it and Terraform's picture of the world goes wrong in ways that surface as baffling plans. Most of what beginners find strange about Terraform stops being strange the moment you hold one sentence: there are three things — the code, the cloud, and the ledger between them.

Figure 12 — The state file as a ledger sitting between the code and the cloud main.tf The code — what you want terraform.tfstate The ledger — what it remembers the third thing people forget exists Azure The cloud — what exists plan compares apply writes
Figure 12 — Code, cloud, and the ledger between Every Terraform decision is a three-way comparison. The code says what you want; the cloud is what exists; the state file records what Terraform believes it built and is the only link between the two. Beginners draw two boxes and wonder why Terraform behaves strangely — the strangeness almost always lives in the third one.
§2

The anatomy of HCL

Terraform files are written in HCL — HashiCorp Configuration Language — and once you have read Bicep, most of it is recognisable at a squint. Here is the Campux VNet from Class Ten, deliberately the same network you wrote in Bicep last class, so the comparison is felt rather than read.

# main.tf — the same VNet, second language
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

data "azurerm_resource_group" "platform" {
  name = "rg-campux-platform"
}

resource "azurerm_virtual_network" "hub" {
  name                = "vnet-hub"
  location            = data.azurerm_resource_group.platform.location
  resource_group_name = data.azurerm_resource_group.platform.name
  address_space       = ["10.20.0.0/16"]
}

output "vnet_id" {
  value = azurerm_virtual_network.hub.id
}
provider
The plug-in that teaches Terraform a platform's API — azurerm for Azure resources, with siblings for Entra ID, AWS, GitHub. Pinning its version is not optional politeness; providers change behaviour between major versions.2
resource
A thing Terraform creates and manages — a type, a local name, and the desired settings. The Bicep resource block wearing different clothes.
data
A thing Terraform reads but does not manage — here, an existing resource group. Data sources are how your code references infrastructure that belongs to someone else without taking ownership of it.
variable / output
Inputs you supply and values handed back — the same in-and-out flow as Bicep's param and output, so the mental model carries straight across.

The distinction worth pausing on is resource versus data. Marking something as a resource means Terraform owns it — it enters the state, and removing the block from your code means Terraform plans to destroy the real thing. A data source carries no such ownership. Mixing the two up is how teams accidentally schedule the deletion of a resource group somebody else built; the code reads almost identically, and the consequence does not.

§3

State — where it lives, why it is sacred

Run terraform apply on your laptop and the ledger appears next to your code as a local file, terraform.tfstate. For a solo experiment that is fine. For a team it is a slow-motion accident: the state on your laptop and the state on your colleague's laptop diverge the first time either of you deploys, and each machine now holds a different account of the same production estate. The fix is a remote backend — the state moves to shared storage that every engineer and every pipeline reads from and writes to. On Azure, that backend is a storage account:

# backend.tf — the ledger moves to shared storage
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate"
    storage_account_name = "campuxtfstate"
    container_name       = "tfstate"
    key                  = "platform.tfstate"
  }
}

Two properties of that arrangement do the real work. First, locking: before any operation that writes state, the blob is automatically locked with a lease, so two engineers who run apply at the same moment cannot both write the ledger — the second is refused until the first finishes. Corrupted state from concurrent writes is the classic team-Terraform disaster, and the Azure backend prevents it by default. Second, the state is itself a secret. It is plain JSON, it records resource attributes, and some of those attributes are sensitive — connection strings, generated passwords, and access keys all land in it in readable text. That is why state never goes into Git, why the storage account holding it is locked down as tightly as a Key Vault, and why .gitignore in every Terraform repository lists *.tfstate before the first commit.

Guard the ledger like a credential.

§4

plan, apply, and drift

The working rhythm of Terraform is two commands. terraform plan is the sibling of Bicep's what-if: it refreshes its picture of the cloud, compares code against state against reality, and prints exactly what it intends to do. terraform apply does it. The symbols carry the same weight they did last class, plus one that is new and dangerous:

$ terraform plan
azurerm_virtual_network.hub: Refreshing state...

  ~ update in-place
  ~ resource "azurerm_virtual_network" "hub" {
        name          = "vnet-hub"
      ~ address_space = ["10.20.0.0/16"] -> ["10.20.0.0/20"]
    }

Plan: 0 to add, 1 to change, 0 to destroy.

+ creates, ~ updates in place, - destroys — and a fourth, -/+, means destroy and recreate. Some properties cannot be changed on a live resource, so Terraform's honest plan is to delete the old one and build a replacement; for a stateless network that is an interruption, for a database it is data loss. Read every -/+ twice before you type yes.

The three-way comparison also gives you drift detection almost for free. Suppose someone "fixes" a setting in the portal on a Tuesday. Your code did not change; the state did not change; the cloud did. The next plan refreshes against reality, sees the cloud disagreeing with the code, and reports it — a ~ line changing the setting back. Run on a schedule, plan becomes a drift alarm for the whole estate: an empty plan is a certificate that production still matches the repository, and a non-empty one names, line by line, where somebody's Tuesday click diverged from the agreed design. That certificate — reality provably matching the reviewed code — is what an employer is actually buying when the job posting says "Terraform".

§5

Bicep or Terraform — the interview answer

You will be asked, in interviews and in meetings, which one is better. The honest answer is that they are the same idea with one structural difference — who keeps the books — and a handful of consequences that follow from it.

Table 1 — The same idea, two sets of books
BicepTerraform
ReachAzure onlyAzure, AWS, GCP, GitHub, and thousands more via providers
BookkeeperAzure itself — ARM compares your file to what existsTerraform's own state file — a third artifact you must store, share, and guard
Previewaz deployment group what-ifterraform plan
New Azure featuresDay one — Bicep is Azure's native surfaceAfter the provider adds support; usually quickly, occasionally not
Operational costNothing extra to runThe state backend: storage, locking, access control

So the interview answer is not a side; it is a decision rule, delivered in about four sentences. Both are declarative, both preview before they apply, and I have written the same network in each. An Azure-only shop should lean Bicep: native, no state to operate, new features on day one. A multi-cloud shop, or one with an existing Terraform estate and module library, should lean Terraform: one language and one workflow across every platform, at the cost of operating state. The skill that transfers is infrastructure as code itself — the tool is a fitting. Candidates who say that get hired by both kinds of shop; candidates who declare one tool dead get filtered by whichever shop runs it happily at scale.3

Case File · Campux Retail

The same VNet, twice on purpose

campux-platform gains a Terraform mirror and a state backend

This class you rebuild a thing that already works, deliberately. The 10.20.0.0/16 VNet that Class Twenty wrote in Bicep gets written again as main.tf — same address space, same design, different language — not because Campux needs it twice, but because you need to feel where the two tools agree and where they part. The parting is state: Bicep needed nothing beyond the file, while Terraform will not take a step until you decide where its ledger lives.

So the first real Terraform work at Campux is not a resource at all. It is a rg-tfstate resource group holding a locked-down storage account, a tfstate container, and a backend "azurerm" block pointing at it — shared state, automatic lease locking, no .tfstate anywhere near Git. Forty stores do not care which language described their network. They care that on the day two engineers deploy at the same minute, the ledger refuses to be written twice.

Watch · Microsoft Learn

The official pages, and a CAMPUX overview

Read the overview first; the state article is the one to work through slowly
Microsoft Learn · Docs

What is Terraform on Azure?
learn.microsoft.com/azure/developer/terraform/overview

Store Terraform state in Azure Storage
learn.microsoft.com/azure/developer/terraform/store-state-in-azure-storage

CAMPUX overview video

A walkthrough of the three-way comparison — code, state, cloud — a plan catching portal drift, and the moment a local state file moves into an Azure storage backend will live here. Video to be added.

Lab 1 · init, plan, apply

The same VNet, second language

~15 minutes · Azure Cloud Shell (Terraform preinstalled) · a subscription you can create resources in

Write the Class Ten VNet in HCL, watch Terraform create its ledger, and read the plan symbols for yourself. Cloud Shell at shell.azure.com ships with Terraform installed, so there is nothing to set up.

  1. Open Cloud Shell (Bash), make a working folder and a throwaway resource group:

    mkdir tf-lab && cd tf-lab
    az group create --name rg-tf-lab --location eastus
    What to notice: Terraform is already there — terraform version proves it. The resource group is disposable; everything lands inside it.
  2. Create main.tf with the §2 configuration, pointed at your lab group (swap the data block's name to rg-tf-lab), then initialise:

    terraform init
    On screen: Terraform downloads the azurerm provider into a hidden .terraform folder. init runs once per folder; it fetches the plug-ins your code declares.
  3. Preview, then apply:

    terraform plan
    terraform apply
    On screen: the plan shows one + for the VNet; apply asks you to type yes before touching anything. When it finishes, run ls — a terraform.tfstate file has appeared. That is the ledger, born.
  4. Open the ledger and look at what it holds:

    grep -A 3 '"type": "azurerm_virtual_network"' terraform.tfstate
    The lesson: plain, readable JSON mapping your resource block to the real Azure id, attributes included. Everything Terraform knows lives in this file — which is exactly why it never goes into Git, and why Lab 2 moves it somewhere safer.
Lab 2 · Remote state & drift

Move the ledger, then catch a portal click

~15 minutes · continues Lab 1 · same Cloud Shell session

Give the state a shared, lease-locked home in a storage account, then create drift on purpose and watch plan catch it.

  1. Create the backend storage (the account name must be globally unique — note the one you get):

    az group create --name rg-tfstate --location eastus
    az storage account create --resource-group rg-tfstate \
      --name campuxtfstate$RANDOM --sku Standard_LRS
    az storage container create --name tfstate \
      --account-name <the-name-you-created>
    What to notice: the ledger's home is ordinary Class Twelve material — a storage account and a blob container. What makes it special is what you are about to put in it.
  2. Add the §3 backend "azurerm" block to main.tf (with your storage account name), then re-initialise:

    terraform init -migrate-state
    On screen: Terraform asks to copy the existing local state into the new backend. Say yes, then remove the leftover local copy: rm terraform.tfstate*. The ledger now lives in the blob — open the container in the portal and it is there.
  3. Now create drift on purpose: in the portal, open vnet-hub and add a tag — say owner : tuesday-click. Then, back in Cloud Shell:

    terraform plan
    On screen: a ~ line on the VNet, planning to remove the tag your code never asked for. Code, state, and cloud disagreed — and plan named exactly where. This is drift detection: the empty plan you did not get is the audit you did not have to run.
  4. Clean up both resource groups when you are done:

    terraform destroy
    az group delete --name rg-tf-lab --yes --no-wait
    az group delete --name rg-tfstate --yes --no-wait
    The lesson: destroy is the ledger read backwards — Terraform deletes exactly what state says it owns, and asks first. Then the groups go, backend included, and the lab has cost you cents.
Note · the storage account and VNet are billable while they exist — pennies for an afternoon, but run the deletes before you walk away. And if plan in step 3 shows nothing, give the portal tag a minute to settle and run it again.
On the job

A source of truth that is not memory

You · Cloud Engineer · a multi-cloud ask

The company picks up a workload on another cloud, and now one tool has to describe both. You reach for Terraform, write the resources in HCL, run a plan so everyone can see the change before it happens, and apply. The state file becomes the team's shared source of truth — and "what is actually deployed?" has an answer that is not someone's memory.

Class Twenty-One

Examination

Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.

Drill 01Recall · state
What does the Terraform state file actually record?
Marked

B. State is the ledger between code and cloud: for every resource block, the real Azure id it corresponds to and the attributes it last saw. It is not your code — that lives in Git — not a command log, and not your login credentials, though it may incidentally contain secrets that resources generate, which is a different and important fact. Mistake state for a backup of the code and you will treat it casually; it is the opposite — the one artifact of the three that cannot be regenerated from the other two.

Drill 02Recall · state
The state file for a working environment is deleted. The Azure resources themselves are untouched. What is Terraform's view of the world now?
Marked

C. The resources still exist, but the link between them and your code is gone — Terraform has amnesia. The next plan proposes to create everything from scratch, and the apply fails on name collisions or, worse, builds duplicates of anything whose name was not unique. Nothing gets auto-deleted (B), and nothing is rebuilt from ARM (D) — Azure keeps no copy of Terraform's books. Recovery means painstakingly re-importing each resource into a fresh state, one by one. This is why the backend storage account is protected like production: the ledger is the one thing you cannot click back into existence.

Drill 03Select three
Which three genuinely live in the Terraform state file?
Marked

Resource ids, attributes (secrets included), and dependency data. The two false options both belong to Git: the configuration lives in the repository, and so does its history — state holds neither. The middle true option is the one to sit with: state is plain JSON, and sensitive attributes land in it readably. That single fact drives half of this class's rules — state never enters Git, the backend storage account is locked down like a vault, and access to read state is access to secrets. If you picked the false options, you were treating state as a copy of the code; it is the opposite — a record of the cloud.

Drill 04Spot the error
This backend configuration is about to be committed to the repository. One line is a serious mistake. Which?
# backend.tf
1.  terraform {
2.    backend "azurerm" {
3.      resource_group_name  = "rg-tfstate"
4.      storage_account_name = "campuxtfstate"
5.      container_name       = "tfstate"
6.      key                  = "platform.tfstate"
7.      access_key           = "kW9xQ2v…rNb8w=="
8.    }
9.  }
Marked

Line seven. That access key is a credential with full control of the storage account holding your state — and state, as Drill 03 established, contains secrets. Committing it hands anyone with repository read access the keys to both. Once pushed it lives in Git history even after the line is deleted; the only real remedy is rotating the key.

The rest of the block is fine — names of resource groups and containers are not secrets (A), the key is just the blob's filename and needs no particular extension (B), and azurerm is exactly the backend this class configures (D). The fix is supplying the credential outside the file: the ARM_ACCESS_KEY environment variable, or better, an identity-based option from Class Nine so there is no key to leak at all. The same lesson as Class Twenty's drill, one tool over: shared code is the last place a secret should live.

Situation 01Write before you reveal
Two engineers, working from the same repository, ran terraform apply at the same time. One apply failed with a state lock error and its owner is annoyed; a third teammate mutters that last year, before the remote backend, the same collision silently corrupted state and cost a weekend. You are asked to untangle what happened and say whether anything is actually wrong.
Ask what the lock error is evidence of — and what the alternative to that error would have been.
Reasoning

The trap is the premise. The annoyed engineer is treating the lock error as a failure. It is the opposite: it is the system working. Before any state write, the Azure backend takes a lease on the state blob; the second apply found the ledger already open on someone else's desk and refused to write to it. The alternative to that error message is exactly the teammate's memory — two writers interleaving into one JSON file, producing a ledger neither of them wrote and a weekend of forensic re-importing. An error that prevents corruption is not an incident.

Say what actually happened, in order. Engineer one's apply acquired the lease and ran to completion; its changes are in state and in Azure. Engineer two's apply asked for the lease, was refused, and changed nothing — no partial write, no half-applied plan. The recovery is one command: engineer two runs terraform plan against the now-updated state and applies if the plan still says what they intend. Their plan may well have changed — engineer one got there first, and the world moved — which is precisely why re-planning, not blindly retrying, is the right reflex.

Then fix the process, not the tool. Two humans racing to apply from their own terminals is the real finding. The locking held, but it is the seatbelt, not the driving. The mature end-state — built over the next three classes — is that only the pipeline applies, one change at a time, from a reviewed plan. Locks should almost never be contested, because production should almost never have two writers.

Situation 02Write before you reveal
An interviewer leans back and asks: "We're a Bicep shop, but half the candidates tell us Terraform is the industry standard and Bicep is a dead end. What's your take?" How do you answer?
They are not asking which tool wins. They are testing whether you reason or recite.
Reasoning

Refuse the tribal framing — out loud. The question is baited: they told you their stack and then quoted people insulting it. Agreeing that Bicep is a dead end insults your interviewer's architecture; pandering that Bicep is superior everywhere is transparently what a candidate would say in that chair. The strong open names the structure instead: both tools are declarative infrastructure as code with a preview step, and the real difference is who keeps the books — Azure itself for Bicep, a state file you must operate for Terraform.

Then give the decision rule and apply it to them. An Azure-only shop leaning Bicep is making a sound call — native support, day-one feature coverage, and no state backend to run and guard. A multi-cloud shop, or one standardising a single workflow across platforms, earns Terraform's operational overhead. They are an Azure shop that has already chosen; the honest reading is that their choice fits the rule. Saying so is not flattery — it is showing you know why their stack is defensible, which is worth more than loyalty to any tool.

Close on the transferable layer. The habits this phase built — desired state in Git, a reviewed diff, a plan read before every apply — are identical in both languages, and you have now written the same VNet in each. A shop's tool can change in a quarter; an engineer who understands the model underneath is useful on either side of the change. That is the sentence that gets remembered after the interview.

Examination record · first attempt
0/4
Class Twenty-One · Complete
Retain this much

Five things worth carrying out of this class

  1. There are three things, not two: the code (what you want), the cloud (what exists), and state — the ledger between them, which Terraform keeps because no single cloud can keep books for every platform it manages.
  2. HCL maps onto what you know: provider (the plug-in), resource (owned and managed), data (read but not owned), variable and output. The resource/data distinction is an ownership decision with destroy consequences.
  3. Team state lives in an Azure storage backend — shared, lease-locked against concurrent writes, and treated as a secret, because sensitive attributes sit in it as plain JSON. It never enters Git.
  4. terraform plan is the three-way comparison made visible: + creates, ~ updates, - destroys, -/+ destroys and recreates. A scheduled plan is a drift alarm; an empty one certifies reality still matches the repository.
  5. The interview answer is a decision rule, not a side: Azure-only leans Bicep (native, no state to operate), multi-cloud leans Terraform (one workflow everywhere, at the cost of running state). The transferable skill is the model, not the tool.
Notes
  1. Strictly, plan first refreshes state against the real cloud, then diffs code against the refreshed state — so the comparison is pairwise under the hood. The three-way picture is still the right mental model, because all three artifacts can independently disagree, and each pairing has its own failure story: code versus cloud is drift, state versus cloud is a stale or damaged ledger, code versus state is your pending change.
  2. The azurerm provider is one of several for the Microsoft estate — azuread manages Entra ID objects, and azapi talks to the ARM REST surface of Class Sixteen directly, for features the main provider has not wrapped yet. Provider major versions genuinely change behaviour, which is why the version = "~> 4.0" pin matters; check the version current when you read this rather than trusting this page's number.
  3. Treat the "industry standard" claims on both sides with suspicion. Terraform genuinely dominates multi-cloud job postings, Bicep genuinely dominates Azure-native shops, and both camps overstate their case; the ground also shifted when HashiCorp relicensed Terraform in 2023 and the OpenTofu fork appeared. What is settled is the direction: declarative IaC with a reviewed plan is the norm, and knowing two dialects of it makes you harder to filter out — whichever one a given shop runs.