Terraform for Azure: Getting Started

Terraform is one of the most widely used Infrastructure as Code tools in the industry. This guide walks through everything you need to get Terraform running against a real Azure subscription — from installing the tool and authenticating, to writing resources and running your first deployment.

What you need before starting

Before writing any Terraform code you need three things on your machine:

  • Terraform CLI. Download from terraform.io or install with a package manager.
  • Azure CLI. Used to authenticate Terraform to your Azure subscription during local development.
  • An Azure subscription. A free trial account works fine for learning.
# Install Terraform on macOS with Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Install on Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# Verify installation
terraform version

# Install Azure CLI (Ubuntu/Debian)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Log in with Azure CLI — Terraform picks up this session automatically
az login

# Set the active subscription
az account set --subscription "My Subscription Name"

Configuring the AzureRM provider

Every Terraform project starts with a provider configuration. For Azure, you use the azurerm provider maintained by HashiCorp. The provider block tells Terraform which version to download and how to authenticate.

Create a new directory for your project, then create these files:

# providers.tf
terraform {
  required_version = ">= 1.7.0"

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

provider "azurerm" {
  features {}
  # In local dev, Terraform uses your active az login session.
  # In CI/CD, set these environment variables instead:
  #   ARM_CLIENT_ID
  #   ARM_CLIENT_SECRET
  #   ARM_TENANT_ID
  #   ARM_SUBSCRIPTION_ID
}
# Download the azurerm provider into .terraform/
terraform init
Note

The features block inside the provider “azurerm” block is required even if it is empty. Terraform will error if you omit it. You can add optional feature flags inside it, such as prevent_deletion_if_contains_resources = true for resource groups.

Writing your first resources

A good first Terraform project creates a resource group, a virtual network, and a storage account. These three resources cover the most common patterns: a container resource, a network resource, and a data resource.

# variables.tf
variable "location" {
  type        = string
  description = "Azure region for all resources"
  default     = "East US"
}

variable "prefix" {
  type        = string
  description = "Prefix used in all resource names"
  default     = "tflearn"
}
# main.tf
resource "azurerm_resource_group" "main" {
  name     = "${var.prefix}-rg"
  location = var.location

  tags = {
    environment = "learning"
    managed_by  = "terraform"
  }
}

resource "azurerm_virtual_network" "main" {
  name                = "${var.prefix}-vnet"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "app" {
  name                 = "app-subnet"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_storage_account" "main" {
  name                     = "${var.prefix}storage001"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  min_tls_version          = "TLS1_2"

  tags = {
    environment = "learning"
    managed_by  = "terraform"
  }
}
# outputs.tf
output "resource_group_name" {
  value = azurerm_resource_group.main.name
}

output "storage_account_name" {
  value = azurerm_storage_account.main.name
}

output "vnet_id" {
  value = azurerm_virtual_network.main.id
}

Running plan, apply, and destroy

The Terraform workflow has three main commands. You run them in order: init once, then plan and apply in a loop as your configuration evolves.

# Always plan before applying — read the output carefully
terraform plan

# Example plan output (abbreviated):
# + azurerm_resource_group.main will be created
# + azurerm_virtual_network.main will be created
# + azurerm_subnet.app will be created
# + azurerm_storage_account.main will be created
#
# Plan: 4 to add, 0 to change, 0 to destroy.

# Apply the changes — Terraform will prompt for confirmation
terraform apply

# Apply with auto-approval (use only in CI/CD, never interactively in prod)
terraform apply -auto-approve

# Show the current state as readable output
terraform show

# List all resources in state
terraform state list

# Tear down everything (prompts for confirmation)
terraform destroy

Authenticating in CI/CD with a Service Principal

In a pipeline, your agents do not have an interactive Azure CLI session. You create a Service Principal — an application identity with specific permissions — and give Terraform its credentials via environment variables.

# Create a Service Principal with Contributor role on a subscription
az ad sp create-for-rbac \
  --name "terraform-sp" \
  --role Contributor \
  --scopes /subscriptions/$(az account show --query id -o tsv)

# Output (save these values securely — you will not see the password again):
# {
#   "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",      <- ARM_CLIENT_ID
#   "displayName": "terraform-sp",
#   "password": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",   <- ARM_CLIENT_SECRET
#   "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"      <- ARM_TENANT_ID
# }

# Get the subscription ID
az account show --query id -o tsv   # <- ARM_SUBSCRIPTION_ID

In your pipeline, set these as secret environment variables:

# Azure Pipelines example — set in pipeline variables, marked as secret
variables:
  ARM_CLIENT_ID: $(TF_ARM_CLIENT_ID)
  ARM_CLIENT_SECRET: $(TF_ARM_CLIENT_SECRET)
  ARM_TENANT_ID: $(TF_ARM_TENANT_ID)
  ARM_SUBSCRIPTION_ID: $(TF_ARM_SUBSCRIPTION_ID)

steps:
  - script: terraform init
    displayName: 'Terraform Init'

  - script: terraform plan -out=tfplan
    displayName: 'Terraform Plan'

  - script: terraform apply tfplan
    displayName: 'Terraform Apply'
Tip

Grant the Service Principal only the permissions it actually needs. Contributor at the subscription level is convenient but broad. For tighter security, use Contributor scoped to a specific resource group, plus additional role assignments only where required (for example, Key Vault access requires a separate RBAC role).

Using terraform.tfvars for environment-specific values

Variables let you separate configuration from code. A terraform.tfvars file provides values for your variables without modifying the variables.tf file itself. This makes it easy to maintain separate values for dev, staging, and production.

# dev.tfvars
location = "East US"
prefix   = "myapp-dev"

# prod.tfvars
location = "West US 2"
prefix   = "myapp-prod"
# Apply with a specific vars file
terraform plan -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"

# Override a single variable on the command line
terraform plan -var="location=North Europe"

Common mistakes

  1. Committing the .terraform directory. The .terraform/ directory contains downloaded provider binaries. Add it to .gitignore along with terraform.tfstate, terraform.tfstate.backup, and any *.tfvars files that contain secrets.
  2. Using a storage account name that is not globally unique. Azure storage account names must be globally unique across all Azure customers, 3-24 characters, lowercase letters and numbers only. Terraform will apply and then fail with an error if your chosen name is already taken. Use a prefix plus a short random suffix.
  3. Skipping the plan step in production. The -auto-approve flag is useful in automated pipelines, but only after you have reviewed the plan output. A common pattern is to save the plan to a file with terraform plan -out=tfplan, review it, then apply exactly that plan with terraform apply tfplan.

Frequently asked questions

How does Terraform authenticate to Azure?

In local development, Terraform uses your Azure CLI login session. In CI/CD pipelines, you authenticate with a Service Principal using client ID and client secret, or with a Managed Identity if running on an Azure-hosted agent. Environment variables like ARM_CLIENT_ID and ARM_CLIENT_SECRET are the standard approach.

What happens if I run terraform apply twice with the same config?

Nothing changes. Terraform compares your configuration against its state file and finds no differences. It reports "No changes. Infrastructure is up-to-date." Terraform deployments are idempotent.

Do I need a paid Terraform plan to use it with Azure?

No. The Terraform CLI is open source and free. You only need a paid HashiCorp plan if you want Terraform Cloud features like remote execution, team collaboration, or Sentinel policy enforcement. For most Azure teams, the open-source CLI with Azure Blob Storage as a state backend is sufficient.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.