All insights

Cloud

Terraform Fundamentals: What a Beginner Actually Needs to Know

Most Terraform tutorials start with syntax. That is the least important part. Here are the six ideas the tool is built on, one working Azure example, and the handful of habits that separate people who use Terraform from people who fight it.

Terraform is a tool for describing infrastructure in text files and having a program make the cloud match that description. That is the whole idea. Everything else — the language, the commands, the ecosystem — exists to serve it.

Beginners tend to struggle not because Terraform is hard, but because they learn commands before they learn what the commands are for. So this article does it the other way round: the ideas first, then the smallest real example that exercises all of them, then the mistakes that cost new teams the most.

1. You describe the destination, not the route

A shell script says do this, then this, then this. Run it twice and it either fails on the second run or creates things twice. That style is imperative.

Terraform is declarative. You write down what should exist — a resource group, a storage account with these settings, a virtual network with these address ranges — and Terraform works out what to do to get there. Run it twice against an estate that already matches and it does nothing, which is the correct answer.

This is the mental shift. You are not writing a program that builds infrastructure. You are writing a description of infrastructure, and Terraform is the program.

2. Providers are how Terraform talks to anything

Terraform itself knows nothing about Azure, AWS, GitHub or DNS. It knows how to read configuration, compute differences and call plugins. The plugins are providers, and there is one for essentially every platform with an API.

On Azure you will meet three:

  • azurerm — Azure resources: resource groups, networks, storage, compute, most of what you think of as “Azure”
  • azuread — Entra ID objects: groups, users, applications, service principals
  • azapi — a thin layer over the Azure REST API, for resource types or properties azurerm does not support yet

A provider is declared once, with a version constraint, and then every resource block in your configuration uses it. Pinning the version is not optional in practice. A provider upgrade can change what Terraform wants to do to your estate, and you want to choose when that happens.

3. Resources are the unit of everything

A resource block describes one thing the provider can create. It has a type, a local name you choose, and arguments:

resource "azurerm_resource_group" "platform" {
  name     = "rg-platform-weu"
  location = "westeurope"
}

Read it as: the azurerm provider should ensure a resource group exists with this name in this location, and in my code I will call it platform.

The local name matters more than beginners expect. It is how other blocks refer to this one:

resource "azurerm_storage_account" "logs" {
  name                     = "stplatformlogsweu"
  resource_group_name      = azurerm_resource_group.platform.name
  location                 = azurerm_resource_group.platform.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

That reference — azurerm_resource_group.platform.name — does two things. It passes a value, obviously. Less obviously, it tells Terraform about a dependency: the storage account cannot be created until the resource group exists, so Terraform will order the operations correctly without you saying so. Most of the ordering in a Terraform configuration is inferred this way. You rarely need to spell it out.

4. The plan is the product

The Terraform workflow is four commands, and one of them is the reason the tool is trusted in production:

terraform init      # download providers, connect to the state backend
terraform plan      # compare configuration with reality; show what would change
terraform apply     # make the changes (after showing the plan again)
terraform destroy   # remove everything this configuration manages

plan is the important one. It reads your configuration, reads the current state, asks the provider what actually exists, and prints a diff: this will be created, this will be updated in place, this will be destroyed and recreated. Nothing happens to the cloud.

That last category is the one to read every time. Some changes cannot be made in place — renaming a storage account, changing a VM’s availability zone — and Terraform’s honest answer is to delete and rebuild. It will tell you. It is your job to notice.

A plan that says 0 to add, 0 to change, 0 to destroy is not a boring result. It is proof that your description and your estate agree, and it is the state you should be in between deliberate changes.

5. State is what makes it all work — and what you must protect

After every apply, Terraform writes a state file: a record of every resource it manages and the real-world identifier for each. It is how Terraform knows, next time, that azurerm_resource_group.platform is the resource group with that ID, rather than a new one that should be created.

Three things follow from that, and each one hurts a beginner who learns it by accident:

State must be shared. If it lives on your laptop, your colleague’s plan does not know what you created and will try to create it again. On Azure, the normal answer is a storage account with a blob container as the backend, with blob leases providing the locking so two people cannot apply at once.

State can contain secrets. A generated password, a connection string, a key — if a resource returns it, Terraform stores it, in plain text. The storage account holding your state deserves the same access control as the systems it describes.

State is Terraform’s memory, not the cloud’s. If someone changes a resource in the portal, the state file is now wrong. The next plan will propose putting things back — which is usually what you want, and occasionally a nasty surprise. This gap between state and reality is called drift, and detecting it is the reason mature teams run a plan on a schedule even when nothing is being changed.

6. Variables, outputs and modules are how it scales

Once a configuration works, three constructs stop it becoming a wall of copy-paste.

Variables are inputs. Declare them with a type and a description, and the same code deploys to development and production with a different values file.

variable "environment" {
  type        = string
  description = "Short environment name used in resource names."
}

Outputs are the values you want back — an IP address, a resource ID, a name another configuration needs.

Modules are folders of Terraform code that you call like a function. A “storage account with our required settings” module means the encryption, network rules and diagnostic settings are decided once, reviewed once, and applied everywhere. Microsoft publishes Azure Verified Modules for exactly this purpose, and using them is a better first step than writing your own.

Beginners should write everything flat first. You only understand what a module is for once you have felt the pain it removes.

A complete first configuration

This is the whole thing: a provider, a resource group, a storage account, an output. It is deliberately small and it runs.

terraform {
  required_version = ">= 1.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

variable "subscription_id" {
  type        = string
  description = "Target Azure subscription."
}

variable "environment" {
  type    = string
  default = "dev"
}

resource "azurerm_resource_group" "platform" {
  name     = "rg-platform-${var.environment}-weu"
  location = "westeurope"

  tags = {
    environment = var.environment
    managed_by  = "terraform"
  }
}

resource "azurerm_storage_account" "logs" {
  name                     = "stlogs${var.environment}weu"
  resource_group_name      = azurerm_resource_group.platform.name
  location                 = azurerm_resource_group.platform.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  min_tls_version          = "TLS1_2"
}

output "storage_account_id" {
  value = azurerm_storage_account.logs.id
}

Sign in with az login, run terraform init, then terraform plan, and read what it proposes: two resources to add, nothing to change, nothing to destroy. Apply it. Then run plan again and watch it report no changes. Then change the min_tls_version, plan, and see that it is an in-place update. Then change the storage account name, plan, and see that it is a replace. You have now seen every kind of change Terraform can make, and you understand more than most people who have used it for a year.

Finish with terraform destroy so nothing bills you overnight.

The mistakes that actually cost teams

Editing in the portal. Every manual change is drift, and every drift is a future plan you have to argue with. Decide on day one that Terraform-managed resources are changed through Terraform, and give the portal read-only access to enforce it.

Local state past the first week. Move to a remote backend before a second person touches the code. Migrating later is possible and unpleasant.

Running apply without reading the plan. The plan is where Terraform tells you it is about to delete the database. It will say so plainly, in red, and it will still do it if you type yes.

Unpinned versions. A provider that updates itself between two pipeline runs is a plan that changes for no reason you made. Pin Terraform, pin every provider, upgrade deliberately.

Secrets in the code. Passwords in .tfvars files find their way into Git. Use a Key Vault reference, a pipeline secret, or a generated value — and remember it will still land in state, which is one more reason the state backend needs locking down.

Renaming a resource in code. Change the local name of a resource block and Terraform sees one resource gone and one new resource to create — a destroy and a rebuild. A moved block tells it that they are the same thing. Learn this before you refactor anything you care about.

Where to go from here

You now know what Terraform is for, how it decides what to do, and what it needs from you in return. The next steps, in order:

  1. Put the state in a storage account backend and make it the only one.
  2. Run plan and apply from a pipeline with a managed identity, not from a laptop with your own credentials.
  3. Adopt Azure Verified Modules for the resource types you deploy most.
  4. Schedule a daily plan against production and treat any drift as a finding.

The tool has plenty more — workspaces, data sources, for_each, dynamic blocks, import, policy checks. None of it is needed until the four steps above are done, and all of it is much easier to learn once they are.

Ready to build a secure, AI-ready Microsoft cloud?

Start with a focused assessment of your cloud, security, AI or FinOps environment.