Logo
CloudWithSingh
Back to all field notes
Concept Map
Terraform
Beginner

Terraform Learning Roadmap with Hands-On Labs

Seventeen CloudLearn labs, in order: first apply, providers, variables, state, modules, then a real Azure stack. Azure and AWS. Share any lab from this page.

Parveen Singh
August 22, 2026
19 min read
Prerequisites:Comfort with a terminalAn Azure or AWS account is useful, but the labs run in a real cloud for youAZ-900 or AWS Cloud Practitioner level helps. It is not required
TLDR

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.

Pro Tip

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:

  1. Write a desired state
  2. Preview the change
  3. Apply it
  4. Keep a record of what exists (state)
  5. Reuse the pattern without copy-paste (modules)
  6. 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

IdeaWhy it matters
ProviderHow Terraform talks to Azure or AWS
Resource blockThe thing you want to exist
PlanThe diff. Read it. Every time.
ApplyMakes the plan real
DestroyTears 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 destroy

init 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
Hands-on LabCloudlearn.io

Introduction to Infrastructure as Code with Terraform on AWS

First AWS resource, first plan, first apply. Start here if you already think in AWS.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

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.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

The Terraform workflow: validate, plan, apply, destroy

The loop you will run hundreds of times. Do this before you care about modules.

labs.cloudlearn.ioStart the lab

Walk out knowing: you read a plan and can say what will be created, changed, or destroyed before you type yes.

Warning

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 init downloads
  • 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

IdeaWhy it matters
Required providersWhich plugin, which version
Lock file.terraform.lock.hcl. Commit it. Same versions on every machine
Resource vs dataCreate vs look up
Arguments vs attributesYou 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
Hands-on LabCloudlearn.io

Terraform providers: configure, version, and lock dependencies

required_providers plus the lock file. Same plugin, same version, every machine.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

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.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

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.

labs.cloudlearn.ioStart the lab

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

IdeaWhy it matters
Input variablesCallers pass values. Defaults are optional
PrecedenceCLI, tfvars, environment, defaults. Know which wins
OutputsWhat the next stack or human needs
Complex typeslist, map, object. Real configs are not all strings
Functionslookup, 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.

Pro Tip

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
Hands-on LabCloudlearn.io

Input variables, outputs, and variable precedence on AWS

One config, many environments. tfvars, -var, and outputs. This is how you stop editing main.tf.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

Complex types and dynamic expressions on AWS

lists, maps, objects, for_each, dynamic. Real configs are not all strings.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

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.

labs.cloudlearn.ioStart the lab

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 .tfvars file, not by editing main.tf
  • You can explain why -var on 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_each or 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

IdeaWhy it matters
State fileTerraform's memory. Lose it and it will try to create duplicates
state list / state showHow you debug "why is it recreating this"
Remote backendAzure Storage or S3. Shared, locked
State lockTwo applies at once will corrupt you. Locking is the fix
Never commit stateSecrets 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.

Warning

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
Hands-on LabCloudlearn.io

Understanding Terraform state fundamentals

state list, state show, what is in the file. Do this before you migrate a backend.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

Terraform state on Azure

The classic Azure backend lab. Remote state without treating the laptop as the source of truth.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

Configure Terraform remote state with Azure Storage

Shared state, lock, Blob backend. This is what a team actually uses.

labs.cloudlearn.ioStart the lab

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

IdeaWhy it matters
Local moduleA folder with its own variables and outputs. The first real reuse
count vs for_eachHow many, and how they are keyed
depends_onLast resort. Prefer a real reference
lifecycleprevent_destroy, ignore_changes, create_before_destroy
Sensitivesensitive = 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
Hands-on LabCloudlearn.io

Introduction to Terraform modules

Folder, variables, outputs. The first reuse pattern, without a full VNet yet.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

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.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

Resource dependencies and lifecycle rules on Azure

Create order, prevent_destroy, ignore_changes. The lab for the Friday apply that should not delete prod.

labs.cloudlearn.ioStart the lab

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
Hands-on LabCloudlearn.io

Managing sensitive data in Terraform configurations

sensitive = true, what still leaks into state, and why tfvars in git is not Key Vault.

labs.cloudlearn.ioStart the lab

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_destroy to 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
Hands-on LabCloudlearn.io

Deploy Azure Container Apps with Terraform

A real compute stack. Container, environment, ingress. The Azure build lab on this path.

labs.cloudlearn.ioStart the lab

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.

#LabCloudPhase
1IaC intro on AWSAWS1. First apply
2Terraform intro on AzureAzure1. First apply
3Workflow: plan, apply, destroyAzure1. First apply
4Providers, versions, lock fileAWS2. Providers
5Resources and data sourcesAzure2. Providers
6Azure Linux VMAzure2. Providers
7Variables, outputs, precedenceAWS3. Variables
8Complex types and dynamicAWS3. Variables
9Built-in functionsAzure3. Variables
10State fundamentalsAzure4. State
11Azure backendAzure4. State
12Remote state on Azure StorageAzure4. State
13Modules introAzure5. Modules
14Local modules for networkingAzure5. Modules
15Dependencies and lifecycleAzure5. Modules
16Sensitive dataAzure5. Modules
17Azure Container AppsAzure6. 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
CourseCloudlearn.io - Interactive Cloud & Security Learning Platform

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.

labs.cloudlearn.ioStart a free lab

What's Next

Bookmark this page

Save it for your next project sprint

Start a project

Apply what you just learned hands-on

Follow on Instagram

Daily cloud tips & behind-the-scenes

Try hands-on labs

Practice in a real cloud environment

Parveen Singh

Parveen Singh

Microsoft Certified Trainer & Cloud Solutions Consultant

Related Field Notes

Found this useful?

Stay in the loop

Weekly cloud insights, no spam

Subscribe

Explore CloudLearn

Hands-on labs & projects

Start Learning

Book Training

Custom cloud training for your team

Get in Touch

On this page