☁️ Azure ⚙️ DevOps 🟪 Terraform 🆕 New

Terraform on Azure: Complete Getting Started Guide

From zero to fully managed Azure infrastructure — install Terraform, authenticate to Azure, build, modify, destroy resources, use variables & outputs, and store state remotely in one end-to-end walkthrough.

📅 7 June 2026 ⏱️ 12 min read 👤 Ravindrakumar 🏷️ Terraform · Azure · IaC
← Back to all posts

1. What is Infrastructure as Code with Terraform?

Infrastructure as Code (IaC) lets you manage cloud resources through configuration files rather than clicking through a portal or running ad-hoc CLI commands. Those files describe what you want — not how to build it — and they can be version-controlled, peer-reviewed, and reused just like application code.

Terraform is HashiCorp's open-source IaC tool. It uses its own declarative language called HCL (HashiCorp Configuration Language) and a rich ecosystem of providers — plugins that translate your .tf files into API calls against a target platform. For Azure, that provider is azurerm.

💡
Key insight

Terraform tracks every resource it manages in a state file (terraform.tfstate). This is what lets it calculate the delta between your desired configuration and what actually exists — making terraform plan the most important safety net in your workflow.

The Terraform workflow

✍️
Write
Author .tf config files describing resources
🔍
terraform init
Download providers & initialise backend
📋
terraform plan
Preview changes before applying
🚀
terraform apply
Create or update real infrastructure
🗑️
terraform destroy
Tear down everything managed by Terraform

2. Installing Terraform & the Azure CLI

You need two things on your machine before writing a single line of HCL: Terraform CLI and the Azure CLI (used for authentication).

Install Terraform on Windows (PowerShell)

The easiest route on Windows is via winget (built into Windows 11 / recent Windows 10):

PowerShell
# Install Terraform via winget
winget install HashiCorp.Terraform

# Verify installation
terraform -version

Alternatively, download the binary from https://releases.hashicorp.com/terraform/, extract it, and add the folder to your $env:PATH.

Install Azure CLI on Windows

PowerShell – Admin
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows `
  -OutFile .\AzureCLI.msi
Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
Remove-Item .\AzureCLI.msi

# Verify
az --version
⚠️
Prerequisites checklist

You need an active Azure subscription (a free account works for these tutorials). If using a paid subscription, some resources may incur charges — always destroy test infrastructure when you are done.

3. Build Your First Azure Resource

Step 1 — Authenticate with Azure CLI

PowerShell
az login
# Your browser opens — sign in with your Azure credentials

# List subscriptions and note the 'id' field
az account list -o table

# Set the subscription you want Terraform to target
az account set --subscription "<SUBSCRIPTION_ID>"

Step 2 — Create a Service Principal

Terraform needs a Service Principal (an Azure AD application identity) so it can authenticate non-interactively in automation pipelines:

PowerShell
az ad sp create-for-rbac `
  --role="Contributor" `
  --scopes="/subscriptions/<SUBSCRIPTION_ID>"

# Output you will receive — store these values securely:
# {
#   "appId":       "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",  ← ARM_CLIENT_ID
#   "password":    "xxxxx~xxxxx~xxxxx",                      ← ARM_CLIENT_SECRET
#   "tenant":      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   ← ARM_TENANT_ID
# }

Step 3 — Set environment variables

Never hardcode credentials in .tf files. Use environment variables so Terraform picks them up automatically via the azurerm provider:

PowerShell
$env:ARM_CLIENT_ID       = "<appId>"
$env:ARM_CLIENT_SECRET   = "<password>"
$env:ARM_SUBSCRIPTION_ID = "<subscription_id>"
$env:ARM_TENANT_ID       = "<tenant>"
💡
Tip — persisting env vars

Set these in your PowerShell $PROFILE or as System environment variables so they survive session restarts. In CI/CD (Azure DevOps / GitHub Actions), store them as pipeline secrets.

Step 4 — Create your Terraform configuration

Create a new folder learn-terraform-azure and add main.tf:

📁 learn-terraform-azure/
  ├── 📄 main.tf  # provider + resource definitions
  ├── 📄 variables.tf  # input variable declarations (added later)
  ├── 📄 outputs.tf  # output value declarations (added later)
  └── 📄 terraform.tfvars  # variable values (gitignore this!)
HCL — main.tf
# Declare the required provider and version constraint
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

# Configure the Azure provider — credentials come from env vars
provider "azurerm" {
  features {}
}

# Create a Resource Group
resource "azurerm_resource_group" "rg" {
  name     = "rg-terraform-demo"
  location = "UK South"
}

Step 5 — Initialise, Plan, and Apply

PowerShell
# Downloads the azurerm provider plugin
terraform init

# Shows what will be created — no changes made yet
terraform plan

# Creates the resource group in Azure
terraform apply
# Type 'yes' when prompted, or use -auto-approve in pipelines

After apply completes, you will see the resource group in the Azure Portal. Terraform also writes a terraform.tfstate file locally — this is your source of truth for what Terraform manages.

⚠️
Never commit terraform.tfstate to Git

The state file can contain sensitive data. Add *.tfstate and *.tfstate.backup to your .gitignore. Use remote state (covered in Section 8) for team workflows.

4. Change Infrastructure

One of Terraform's most powerful features is its ability to calculate the minimum set of changes needed to move from the current state to your desired state. You never need to remember what you provisioned before — the state file does that for you.

Let's add a Virtual Network to our resource group. Update main.tf:

HCL — main.tf (updated)
# ... (provider block remains unchanged) ...

resource "azurerm_resource_group" "rg" {
  name     = "rg-terraform-demo"
  location = "UK South"
}

# NEW — Virtual Network inside the same resource group
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-terraform-demo"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.0.0.0/16"]

  tags = {
    Environment = "Demo"
    ManagedBy   = "Terraform"
  }
}
PowerShell
terraform plan
# Plan: 1 to add, 0 to change, 0 to destroy
# The existing resource group is unchanged

terraform apply

Notice that terraform plan only shows + (add) for the new VNet — it does not touch the resource group already in state. This is how Terraform handles incremental changes safely.

Understanding change symbols in plan output

SymbolMeaningImpact
+CreateNew resource will be provisioned
~Update in-placeResource modified without recreation
-/+Destroy and recreateResource must be replaced (watch for this!)
-DestroyResource will be deleted
⚠️
Watch out for -/+ (replace)

Some attribute changes (like renaming a resource or changing an immutable property) force a destroy-and-recreate cycle. Always read the plan output carefully before typing yes, especially in production.

5. Destroy Infrastructure

When you no longer need your test environment, terraform destroy removes everything Terraform has provisioned — in the correct dependency order, automatically. It is the clean, safe alternative to manually deleting resources one by one from the portal.

PowerShell
# Preview what will be destroyed
terraform plan -destroy

# Destroy all managed resources
terraform destroy
# Review the plan output, then type 'yes' to confirm

# Plan: 0 to add, 0 to change, 2 to destroy.
# Terraform will delete vnet first, then the resource group
💡
Targeted destroy

To destroy only a specific resource without touching others, use the -target flag:
terraform destroy -target azurerm_virtual_network.vnet
This is useful in development but avoid it in production as it can leave state inconsistent.

6. Input Variables — Making Configs Reusable

Hardcoded values in main.tf make configs brittle and environment-specific. Input variables are Terraform's mechanism for parameterising configurations — the same code can deploy to dev, staging, and production by simply changing variable values.

Declaring variables in variables.tf

HCL — variables.tf
variable "resource_group_name" {
  description = "Name of the Azure resource group"
  type        = string
  default     = "rg-terraform-demo"
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "UK South"
}

variable "vnet_address_space" {
  description = "CIDR block for the virtual network"
  type        = list(string)
  default     = ["10.0.0.0/16"]
}

variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Referencing variables in main.tf

HCL — main.tf (with variables)
resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-${var.environment}-demo"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = var.vnet_address_space
}

Supplying variable values

MethodHowBest for
Default in declarationdefault = "value"Non-sensitive shared defaults
terraform.tfvars filelocation = "UK South"Local / environment-specific values
-var flagterraform apply -var="environment=prod"One-off overrides
Environment variable$env:TF_VAR_environment = "prod"CI/CD pipelines
💡
Sensitive variables

Mark secrets (passwords, keys) with sensitive = true in the variable declaration. Terraform will redact the value in plan and apply output, keeping it out of logs.

7. Output Values — Exposing Useful Information

After terraform apply, certain values generated by Azure (resource IDs, IP addresses, connection strings) are buried inside the state file. Output values surface them to the terminal and make them available for other Terraform configurations to consume.

Declaring outputs in outputs.tf

HCL — outputs.tf
output "resource_group_id" {
  description = "The Azure resource ID of the resource group"
  value       = azurerm_resource_group.rg.id
}

output "resource_group_name" {
  description = "Name of the created resource group"
  value       = azurerm_resource_group.rg.name
}

output "vnet_id" {
  description = "Resource ID of the virtual network"
  value       = azurerm_virtual_network.vnet.id
}

output "vnet_address_space" {
  description = "Address space of the virtual network"
  value       = azurerm_virtual_network.vnet.address_space
}

Working with outputs

PowerShell
# Show all outputs after apply
terraform output

# Query a specific output value
terraform output resource_group_id

# Output as JSON (useful in CI/CD pipelines)
terraform output -json

# Example output:
# resource_group_id = "/subscriptions/.../resourceGroups/rg-terraform-demo"
# vnet_id           = "/subscriptions/.../virtualNetworks/vnet-dev-demo"
💡
Cross-module references

In larger configurations, outputs from one Terraform module can be referenced as inputs to another using module.<name>.<output_name> syntax. This is how you compose complex environments from reusable building blocks.

8. Remote State with HCP Terraform

The local terraform.tfstate file is fine for learning, but it creates serious problems in teams: merge conflicts, no locking, and accidental overwrites. The solution is to store state remotely. HashiCorp's managed service HCP Terraform (formerly Terraform Cloud) provides free remote state storage with state locking and a web UI for plan history.

Set up HCP Terraform (free tier)

1

Create an account

Sign up at app.terraform.io and create an Organisation.

2

Create a Workspace

Choose CLI-driven workflow — this lets you run terraform apply locally while state is stored remotely.

3

Generate an API token

In HCP Terraform: User Settings → Tokens → Create an API token. Copy it — you will only see it once.

4

Authenticate CLI

Run terraform login — it opens a browser and saves the token to your local credentials file.

Configure the cloud backend in main.tf

HCL — main.tf (with remote backend)
terraform {
  cloud {
    organization = "your-org-name"   # Replace with your HCP Terraform org

    workspaces {
      name = "learn-terraform-azure"  # The workspace you created
    }
  }

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

provider "azurerm" {
  features {}
}
PowerShell
# Re-initialise to migrate state to the cloud backend
terraform init

# Terraform will detect existing local state and ask if you want to migrate
# Type 'yes' — your state is now stored securely in HCP Terraform

# All subsequent plan/apply operations work exactly the same
terraform plan
terraform apply

Setting ARM credentials in HCP Terraform workspace

When using remote operations in HCP Terraform, the Azure credentials are not in your local shell — they need to be configured as Environment Variables in the workspace settings:

Variable nameValue sourceSensitive?
ARM_CLIENT_IDService Principal appIdNo
ARM_CLIENT_SECRETService Principal password✅ Yes
ARM_SUBSCRIPTION_IDYour Azure subscription IDNo
ARM_TENANT_IDYour Azure tenant IDNo
💡
Alternative: Azure Blob Storage backend

If you prefer to keep state within Azure itself rather than HCP Terraform, use the azurerm backend with a Storage Account. This is common in enterprise setups and integrates well with Azure RBAC and Private Endpoints. Configure it with backend "azurerm" { ... } in your terraform block.

← Back to all posts