Seventeen CloudLearn labs, in the order I would actually learn Terraform. Three first-apply labs so Azure and AWS people both have a start. Then providers, variables, state, modules, secrets, and a real Container Apps stack. Do them in order. Skip a lab only if the checkpoint already holds. The Terraform CLI note is the command lookup. This page is the path.
πΊοΈ How this roadmap works
Same shape as the Bicep learning roadmap. Concept, a small snippet you can copy, then a lab in a real cloud. Every lab on this page is its own stop. That is on purpose. Share the section, not a dump of links.
Each lab:
- π What to understand before you click
- π§ͺ The lab that locks it in
- β Walk out knowing so you know you can move
Estimated time: 4 to 5 weeks at an hour or two a day if you do all seventeen. Faster if you already write ARM, Bicep, or CloudFormation.
Pick one cloud for the first three labs. Azure or AWS, not both. The language is the same. The provider docs are not. Mixing them on day one is how people spend a week debugging auth instead of learning HCL. After state, mix freely.
π§ Why Terraform, and why a path
Job postings still say Terraform more than they say Bicep. Multi-cloud teams, AWS shops, and a lot of Azure shops that started before Bicep matured. If you want one IaC skill that travels, this is it.
What you are actually learning is not syntax. It is:
- Write a desired state
- Preview the change
- Apply it
- Keep a record of what exists (state)
- Reuse the pattern without copy-paste (modules)
- Ship a stack that is more than a storage account
Skip 4 and 5 and you will still pass a "write a resource block" screen. You will fail the "this already exists, don't recreate it" screen.
The HashiCorp Certified Terraform Associate (004) maps onto this path. Treat the cert as a byproduct. The labs are the point.
π Phase 1: First apply
Time: 4 to 6 hours | Goal: init, plan, apply, destroy on a real resource, on the cloud you actually use
Three labs. Same loop, three different first resources. Do the one that matches your cloud, then do the workflow lab. The workflow lab is the one I would keep if I could only keep one.
What you need to be able to say
| Idea | Why it matters |
|---|---|
| Provider | How Terraform talks to Azure or AWS |
| Resource block | The thing you want to exist |
| Plan | The diff. Read it. Every time. |
| Apply | Makes the plan real |
| Destroy | Tears it down. You will forget this once. You will not forget the bill. |
The loop is always this:
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform destroyinit downloads providers. plan is the preview. apply is the commit. destroy is how you do not wake up to an invoice.
π§ͺ Lab 1: Introduction to IaC on AWS
If you think in AWS already, start here. First resource, first plan, first apply. You walk out knowing that a .tf file is a desired state, not a script.

Introduction to Infrastructure as Code with Terraform on AWS
First AWS resource, first plan, first apply. Start here if you already think in AWS.
Walk out knowing: you can explain what init downloaded, and you destroyed the resource before you closed the tab.
π§ͺ Lab 2: Introduction to Terraform on Azure
Same idea, Azure. A slower first apply if AWS is not your cloud yet. Do this even if you already did the AWS intro. The provider block is the only part that should feel new.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}That required_providers block is how you stop "works on my laptop, different plugin on CI."

Introduction to Terraform on Azure
First Azure resource end to end. Use this if Azure is the job, or as a second first-apply after AWS.
Walk out knowing: you can point at required_providers and the provider block and say what each is for.
π§ͺ Lab 3: The Terraform workflow
This is the lab. fmt β validate β plan β apply β destroy, on purpose, not as a side effect of a tutorial. If you only send someone one link from this page, send this one.

The Terraform workflow: validate, plan, apply, destroy
The loop you will run hundreds of times. Do this before you care about modules.
Walk out knowing: you read a plan and can say what will be created, changed, or destroyed before you type yes.
terraform apply -auto-approve on your laptop is a habit you will take into a pipeline. Do not start it. Type yes until you are in CI with a saved plan file.
β Phase 1 checkpoint
- You can explain what
initdownloads - You read a plan and can say what will be created
- You applied, verified in the portal, then destroyed
- You did not leave a resource running overnight "to look at later"
π§± Phase 2: Providers, resources, data sources
Time: 5 to 7 hours | Goal: pin versions, write a real resource, look something up, deploy a VM
A resource is what you create. A data source is what you read. Mixing them up is how people try to "create" a VNet that already exists.
What you need to be able to say
| Idea | Why it matters |
|---|---|
| Required providers | Which plugin, which version |
| Lock file | .terraform.lock.hcl. Commit it. Same versions on every machine |
| Resource vs data | Create vs look up |
| Arguments vs attributes | You set arguments. Terraform fills attributes after apply |
data "azurerm_resource_group" "this" {
name = var.resource_group_name
}
resource "azurerm_storage_account" "this" {
name = var.storage_name
resource_group_name = data.azurerm_resource_group.this.name
location = data.azurerm_resource_group.this.location
account_tier = "Standard"
account_replication_type = "LRS"
}The data source is the lookup. The resource is the create. If the resource group already exists, do not declare a second one.
π§ͺ Lab 4: Providers, versions, and the lock file
People skip .terraform.lock.hcl and then CI uses a different provider than their laptop. This lab is the fix. Pin it. Commit it. Stop being surprised.

Terraform providers: configure, version, and lock dependencies
required_providers plus the lock file. Same plugin, same version, every machine.
Walk out knowing: you can point at required_providers and .terraform.lock.hcl and say what each is for.
π§ͺ Lab 5: Resources and data sources on Azure
Write a resource, then read something that already exists. That split is the whole phase. After this, you should stop hardcoding IDs you copied from the portal.

Terraform resources and data sources on Azure
Create vs look up. This is the lab that stops you from 'creating' a VNet that already exists.
Walk out knowing: you used a data source instead of pasting a resource ID into main.tf.
π§ͺ Lab 6: A Linux VM with Terraform
Storage accounts are fine for syntax. A VM is a resource people actually care about: NIC, NSG, public IP, disk. This is the first stack that looks like a job, not a hello-world.

Create an Azure virtual machine using Terraform
A real compute resource, not a storage account. NIC, image, size, the bits a posting will ask about.
Walk out knowing: you can name every resource that had to exist before the VM could boot.
β Phase 2 checkpoint
- You committed a lock file, or you can explain why the lab environment did
- You can write a resource block without copying a whole tutorial file
- You used a data source instead of hardcoding an ID
- You deployed a VM and destroyed it
ποΈ Phase 3: Variables, types, and expressions
Time: 5 to 7 hours | Goal: one config, many environments
Hardcoded names and SKUs are how a "works on my subscription" template dies in someone else's. Variables, outputs, and functions are how one config serves dev and prod without a fork.
What you need to be able to say
| Idea | Why it matters |
|---|---|
| Input variables | Callers pass values. Defaults are optional |
| Precedence | CLI, tfvars, environment, defaults. Know which wins |
| Outputs | What the next stack or human needs |
| Complex types | list, map, object. Real configs are not all strings |
| Functions | lookup, merge, cidrsubnet, toset. Small set. High leverage |
variable "environment" {
type = string
default = "dev"
}
variable "cidrs" {
type = map(string)
}
output "storage_id" {
value = azurerm_storage_account.this.id
sensitive = false
}Pass values with a .tfvars file. Do not edit main.tf every time the environment changes.
Name things with a prefix plus a hash of something stable (random_id, or a workspace name), not mystorageaccount123 that you will fight globally on the second apply.
π§ͺ Lab 7: Input variables, outputs, and precedence on AWS
This is the lab that stops you from forking the repo for staging. Precedence is the part people guess wrong in interviews. CLI beats tfvars beats defaults. Know the order, don't memorize a blog post.

Input variables, outputs, and variable precedence on AWS
One config, many environments. tfvars, -var, and outputs. This is how you stop editing main.tf.
Walk out knowing: you passed values with a .tfvars file, and you read an output with terraform output.
π§ͺ Lab 8: Complex types and dynamic expressions on AWS
for_each, dynamic, objects. This is where HCL stops feeling like JSON with extra quotes. If your config is still a pile of copied resource blocks, you are not done with this phase.
resource "aws_subnet" "this" {
for_each = var.subnets
vpc_id = aws_vpc.this.id
cidr_block = each.value.cidr
availability_zone = each.value.az
}Keyed by a stable map key, not count and an index that shifts when you delete item 0.

Complex types and dynamic expressions on AWS
lists, maps, objects, for_each, dynamic. Real configs are not all strings.
Walk out knowing: you replaced a count index with a for_each key you could explain.
π§ͺ Lab 9: Built-in functions on Azure
You keep reaching for bash to transform a string. Don't. lookup, merge, join, cidrsubnet, toset cover most of what shows up in real files.

Terraform built-in functions and expressions in practice
The small set of functions that show up in real configs. Stop shelling out to transform a string.
Walk out knowing: you used a function in the config instead of computing the value by hand and pasting it in.
β Phase 3 checkpoint
- You passed values with a
.tfvarsfile, not by editingmain.tf - You can explain why
-varon the CLI beats a committed secret, and why a tfvars in git still is not a vault - You returned an output and read it with
terraform output - You used
for_eachor a function on purpose, not by accident from a copy-paste
π¦ Phase 4: State
Time: 4 to 6 hours | Goal: know what state is, then put it somewhere that locks
State is the map between your code and the real resources. Local terraform.tfstate is fine for a lab you will destroy in an hour. It is not fine for a team, a pipeline, or anything you care about.
Three labs, in this order: inspect state so it is not magic, the older Azure backend lab if you want the classic version, then remote state on Azure Storage so it is not a file on a laptop.
What you need to be able to say
| Idea | Why it matters |
|---|---|
| State file | Terraform's memory. Lose it and it will try to create duplicates |
state list / state show | How you debug "why is it recreating this" |
| Remote backend | Azure Storage or S3. Shared, locked |
| State lock | Two applies at once will corrupt you. Locking is the fix |
| Never commit state | Secrets land in it. Git is not your backend |
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstateprod"
container_name = "tfstate"
key = "app.terraform.tfstate"
}
}That block is the production pattern. The local file was a footgun.
terraform state rm does not delete the cloud resource. It makes Terraform forget it. The next person to apply may create a second one, or ignore the original until it becomes an invoice. Know which one you wanted.
π§ͺ Lab 10: State fundamentals
Open the box. See what Terraform thinks exists before you trust a remote backend. If state is still magic after this, do not move on.

Understanding Terraform state fundamentals
state list, state show, what is in the file. Do this before you migrate a backend.
Walk out knowing: you can describe what is in state without opening the Azure portal.
π§ͺ Lab 11: Terraform state with an Azure backend
The older Azure backend lab. Same idea as lab 12, classic catalog version. Do it if you already started here, or if you want a second pass before the newer remote-state lab.

Terraform state on Azure
The classic Azure backend lab. Remote state without treating the laptop as the source of truth.
Walk out knowing: you can explain why git is not a backend.
π§ͺ Lab 12: Remote state on Azure Storage
The production pattern. Blob backend, lock, and why the old local file was a footgun. If you do one state lab after fundamentals, do this one.

Configure Terraform remote state with Azure Storage
Shared state, lock, Blob backend. This is what a team actually uses.
Walk out knowing: you know what a lock error means, and you do not force-unlock as a first move.
β Phase 4 checkpoint
- You can describe what is in state without opening Azure
- You migrated off a local file to a remote backend
- You know what a lock error means
π§© Phase 5: Modules, lifecycle, and secrets
Time: 6 to 8 hours | Goal: reuse a pattern, protect a resource, stop putting keys in git
This is the production language phase. Modules so you stop copy-pasting a VNet. Lifecycle so Terraform does not destroy the one resource you cannot recreate on a Friday. Sensitive values so the plan output is not a password dump.
What you need to be able to say
| Idea | Why it matters |
|---|---|
| Local module | A folder with its own variables and outputs. The first real reuse |
count vs for_each | How many, and how they are keyed |
depends_on | Last resort. Prefer a real reference |
lifecycle | prevent_destroy, ignore_changes, create_before_destroy |
| Sensitive | sensitive = true on variables and outputs. Still in state. Still not for git |
module "network" {
source = "./modules/network"
prefix = var.prefix
address_space = ["10.20.0.0/16"]
location = var.location
resource_group = var.resource_group_name
}
resource "azurerm_key_vault" "this" {
# ...
lifecycle {
prevent_destroy = true
}
}A module is a contract: inputs in, IDs out. prevent_destroy is how you do not delete Key Vault on a Friday.
π§ͺ Lab 13: Terraform modules intro
Shorter first pass. Folder shape, inputs, outputs. Do this before the networking module if modules still feel like "advanced Terraform."

Introduction to Terraform modules
Folder, variables, outputs. The first reuse pattern, without a full VNet yet.
Walk out knowing: a module is a folder with a contract, not a special file type.
π§ͺ Lab 14: Local modules for Azure networking
Split a VNet into a module. Pass a prefix in, get IDs out. Call it twice with different inputs. That is the contract. This is the modules lab I would show in an interview.

Create and use local Terraform modules for Azure networking
A VNet module you can call twice. This is reuse, not copy-paste with the names changed.
Walk out knowing: you called a local module twice with different inputs.
π§ͺ Lab 15: Resource dependencies and lifecycle
Things create in the wrong order, or destroy when they must not. depends_on is a last resort. Prefer a real reference. lifecycle is how you protect the resource you cannot recreate from a backup.

Resource dependencies and lifecycle rules on Azure
Create order, prevent_destroy, ignore_changes. The lab for the Friday apply that should not delete prod.
Walk out knowing: you can explain prevent_destroy to someone who is about to apply on prod.
π§ͺ Lab 16: Managing sensitive data
You still have a password in terraform.tfvars. Mark it sensitive. Know that it still lands in state. Git is still not a vault. This lab is the one that makes that stick.

Managing sensitive data in Terraform configurations
sensitive = true, what still leaks into state, and why tfvars in git is not Key Vault.
Walk out knowing: you marked a variable sensitive and confirmed it does not print in the plan.
β Phase 5 checkpoint
- You called a local module twice with different inputs
- You can explain
prevent_destroyto someone who is about to apply on prod - You marked a variable sensitive and confirmed it does not print in the plan
ποΈ Phase 6: A real Azure stack
Time: 2 to 3 hours | Goal: ship something you would put on a resume
Language without a stack is still a tutorial. Container Apps is the Completed Azure build lab on this path: a container, not a storage account.
You already deployed a VM in lab 6. This is the next step up: a managed container that actually serves traffic.
π§ͺ Lab 17: Azure Container Apps with Terraform
Revision, ingress, a real compute plane. If someone asks "have you deployed anything besides a VM with Terraform," this is the link.

Deploy Azure Container Apps with Terraform
A real compute stack. Container, environment, ingress. The Azure build lab on this path.
Walk out knowing: you can point at the plan and say which resource owns the public endpoint, and you destroyed the stack.
β Phase 6 checkpoint
- You deployed a stack with more than one resource type
- You can say which resource owns the public endpoint
- You destroyed it. Resume screenshots from a live lab you left running are not a flex.
πΊοΈ The path at a glance
Seventeen labs. Share any row.
| # | Lab | Cloud | Phase |
|---|---|---|---|
| 1 | IaC intro on AWS | AWS | 1. First apply |
| 2 | Terraform intro on Azure | Azure | 1. First apply |
| 3 | Workflow: plan, apply, destroy | Azure | 1. First apply |
| 4 | Providers, versions, lock file | AWS | 2. Providers |
| 5 | Resources and data sources | Azure | 2. Providers |
| 6 | Azure Linux VM | Azure | 2. Providers |
| 7 | Variables, outputs, precedence | AWS | 3. Variables |
| 8 | Complex types and dynamic | AWS | 3. Variables |
| 9 | Built-in functions | Azure | 3. Variables |
| 10 | State fundamentals | Azure | 4. State |
| 11 | Azure backend | Azure | 4. State |
| 12 | Remote state on Azure Storage | Azure | 4. State |
| 13 | Modules intro | Azure | 5. Modules |
| 14 | Local modules for networking | Azure | 5. Modules |
| 15 | Dependencies and lifecycle | Azure | 5. Modules |
| 16 | Sensitive data | Azure | 5. Modules |
| 17 | Azure Container Apps | Azure | 6. Build |
You do not need a different path for Azure vs AWS. The language labs are mixed on purpose. Use lab 1 and 4 and 7 and 8 if the posting is AWS. Use lab 2 and 3 and 5 and 6 if the posting is Azure. Everyone does state and modules.
What's next
After this:
- More Azure stacks - Key Vault, storage lifecycle, App Service TLS, LB plus VMSS. Those labs exist. They are not on this page until they are in the same Completed set as these seventeen.
- More AWS stacks - S3 sites, Lambda APIs. Same rule.
- CI - plan on the pull request, apply after review. GitHub Actions plus OIDC, not a stored ARM secret
- Bicep - if you live in Azure only, the Bicep roadmap is the native path. Learn Terraform first if the posting says Terraform
- The CLI note - Terraform CLI Field Notes when you forget a flag, not when you forget the order

CloudLearn: every lab in this roadmap
Real Azure and AWS environments in the browser. No subscription of your own. The same catalog coaching students get.