Terraform on Azure: Complete Getting Started Guide for 2026

Category Recommendation
Best Practice Use service principals for authentication, organize resources with modules
Security Store state files in encrypted Azure Storage, never commit secrets to Git
Performance Use remote state backends, enable state locking with Azure Storage
Cost Optimization Tag all resources, use terraform plan before apply, implement lifecycle rules
Monitoring Enable Azure Activity Log integration, use Terraform Cloud for team collaboration

TL;DR

  • Terraform transforms Azure infrastructure management from manual clicks to reproducible, version-controlled code
  • Authentication via service principals provides secure, automated access to Azure resources without human intervention
  • Remote state storage in Azure Blob Storage enables team collaboration and prevents configuration drift
  • Resource tagging and lifecycle management keeps costs predictable and environments organized
  • Modular configuration patterns make complex Azure deployments maintainable and reusable across projects

Introduction

Managing Azure infrastructure through the portal becomes unwieldy as your cloud footprint grows. Clicking through menus, remembering configuration details, and ensuring consistency across environments quickly turns into a full-time job fraught with human error.

flowchart LR
  Config[".tf configuration"] --> Init["terraform init"]
  Init --> Plan["terraform plan"]
  Plan --> Apply["terraform apply"]
  Apply --> State[("State - azurerm backend")]
  Apply --> Azure["Azure resources"]
  State -.refresh.-> Plan
Figure: The core Terraform workflow against an Azure remote-state backend.

Terraform solves this infrastructure-as-code challenge by treating your Azure resources as declarative configuration files. You describe what you want, Terraform figures out how to make it happen, and you get reproducible deployments that can be version-controlled, peer-reviewed, and automated.

This terraform azure tutorial walks you through building real Azure infrastructure—from a simple resource group to a complete web application stack. You'll learn authentication best practices, state management, and the modular patterns that make Terraform maintainable at scale. By the end, you'll have hands-on experience with the tools and techniques used by Azure DevOps teams worldwide.

Prerequisites

  • Azure subscription with Contributor or Owner role
  • Azure CLI 2.55.0 or later installed and configured
  • Terraform 1.6.0+ installed (latest stable recommended)
  • Basic understanding of Azure resource hierarchy (subscriptions, resource groups)
  • Command-line comfort with Bash or PowerShell
  • Git installed for version control (optional but recommended)

Setting Up Terraform Azure Authentication

Creating a Service Principal

The most secure way to authenticate Terraform with Azure is through a service principal—essentially a non-human identity that Terraform uses to manage resources.

# Login to Azure CLI
az login

# Get your subscription ID
az account show --query id --output tsv

# Create service principal with Contributor role
az ad sp create-for-rbac \
  --name "terraform-sp" \
  --role="Contributor" \
  --scopes="/subscriptions/YOUR_SUBSCRIPTION_ID"

Save the output securely—you'll need these values:

{
  "appId": "12345678-1234-1234-1234-123456789012",
  "displayName": "terraform-sp",
  "password": "super-secret-password",
  "tenant": "87654321-4321-4321-4321-210987654321"
}

Configuring Environment Variables

Set these environment variables to authenticate Terraform:

# Linux/macOS
export ARM_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
export ARM_TENANT_ID="87654321-4321-4321-4321-210987654321"
export ARM_CLIENT_ID="12345678-1234-1234-1234-123456789012"
export ARM_CLIENT_SECRET="super-secret-password"
# PowerShell
$env:ARM_SUBSCRIPTION_ID = "YOUR_SUBSCRIPTION_ID"
$env:ARM_TENANT_ID = "87654321-4321-4321-4321-210987654321"
$env:ARM_CLIENT_ID = "12345678-1234-1234-1234-123456789012"
$env:ARM_CLIENT_SECRET = "super-secret-password"

Your First Terraform Azure Configuration

Project Structure Setup

Create a clean project structure for your terraform azure tutorial:

mkdir terraform-azure-demo
cd terraform-azure-demo

# Create essential files
touch main.tf variables.tf outputs.tf terraform.tf

Basic Configuration Files

terraform.tf – Provider and backend configuration:

terraform {
  required_version = ">= 1.6.0"
  
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.80"
    }
  }
}

provider "azurerm" {
  features {}
}

variables.tf – Input parameters:

variable "resource_group_name" {
  description = "Name of the Azure Resource Group"
  type        = string
  default     = "rg-terraform-demo"
}

variable "location" {
  description = "Azure region for resources"
  type        = string
  default     = "East US"
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"
}

variable "project_name" {
  description = "Project identifier for resource naming"
  type        = string
  default     = "tfdemo"
}

main.tf – Core resource definitions:

# Create Resource Group
resource "azurerm_resource_group" "main" {
  name     = var.resource_group_name
  location = var.location

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
    CreatedDate = timestamp()
  }
}

# Create Virtual Network
resource "azurerm_virtual_network" "main" {
  name                = "${var.project_name}-vnet-${var.environment}"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}

# Create Subnet
resource "azurerm_subnet" "web" {
  name                 = "${var.project_name}-subnet-web-${var.environment}"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]
}

outputs.tf – Return values:

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

output "virtual_network_id" {
  description = "ID of the created virtual network"
  value       = azurerm_virtual_network.main.id
}

output "subnet_id" {
  description = "ID of the created subnet"
  value       = azurerm_subnet.web.id
}

Deploying Your First Resources

Initialize and deploy your Terraform configuration:

# Initialize Terraform (downloads providers)
terraform init

# Validate configuration syntax
terraform validate

# Preview changes
terraform plan

# Apply changes (create resources)
terraform apply

When prompted, type yes to confirm the deployment. Terraform will create your resource group, virtual network, and subnet in Azure.

Building a Complete Web Application Stack

Now let's extend our terraform azure tutorial to include a realistic web application infrastructure with load balancer, virtual machines, and database.

Add to main.tf:

# Create Network Security Group
resource "azurerm_network_security_group" "web" {
  name                = "${var.project_name}-nsg-web-${var.environment}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  security_rule {
    name                       = "HTTP"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "HTTPS"
    priority                   = 1002
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}

# Associate NSG with Subnet
resource "azurerm_subnet_network_security_group_association" "web" {
  subnet_id                 = azurerm_subnet.web.id
  network_security_group_id = azurerm_network_security_group.web.id
}

# Create Public IP for Load Balancer
resource "azurerm_public_ip" "lb" {
  name                = "${var.project_name}-pip-lb-${var.environment}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  allocation_method   = "Static"
  sku                 = "Standard"

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}

# Create Load Balancer
resource "azurerm_lb" "main" {
  name                = "${var.project_name}-lb-${var.environment}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  sku                 = "Standard"

  frontend_ip_configuration {
    name                 = "PublicIPAddress"
    public_ip_address_id = azurerm_public_ip.lb.id
  }

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}

# Create MySQL Flexible Server
resource "azurerm_mysql_flexible_server" "main" {
  name                   = "${var.project_name}-mysql-${var.environment}"
  resource_group_name    = azurerm_resource_group.main.name
  location               = azurerm_resource_group.main.location
  administrator_login    = "mysqladmin"
  administrator_password = "P@ssw0rd123!"
  sku_name               = "B_Standard_B1s"
  version                = "8.0.21"

  storage {
    size_gb = 20
  }

  tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}

Implementing Remote State Management

Creating Azure Storage for State

Remote state storage prevents conflicts in team environments and provides backup for your Terraform state:

# Create storage account for Terraform state
az group create --name "rg-terraform-state" --location "East US"

az storage account create \
  --name "tfstate$RANDOM" \
  --resource-group "rg-terraform-state" \
  --location "East US" \
  --sku "Standard_LRS" \
  --encryption-services blob

# Get storage account key
ACCOUNT_KEY=$(az storage account keys list \
  --resource-group "rg-terraform-state" \
  --account-name "tfstate$RANDOM" \
  --query '[0].value' -o tsv)

# Create container for state files
az storage container create \
  --name "terraform-state" \
  --account-name "tfstate$RANDOM" \
  --account-key $ACCOUNT_KEY

Configuring Backend

Update terraform.tf to use remote state:

terraform {
  required_version = ">= 1.6.0"
  
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "tfstateYOUR_RANDOM_NUMBER"
    container_name       = "terraform-state"
    key                  = "dev.terraform.tfstate"
  }
  
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.80"
    }
  }
}

Migrate existing state to remote backend:

terraform init -migrate-state

Troubleshooting Common Issues

Authentication Failures

Error: Error: building AzureRM Client: Authenticating using the Azure CLI is only supported as a User

Fix: Ensure service principal environment variables are set correctly:

# Verify variables are set
echo $ARM_CLIENT_ID
echo $ARM_TENANT_ID

# Re-export if empty
export ARM_CLIENT_ID="your-app-id"
export ARM_CLIENT_SECRET="your-password"

Resource Naming Conflicts

Error: Error: A resource with the ID already exists

Fix: Use unique naming patterns with random suffixes:

resource "random_id" "suffix" {
  byte_length = 4
}

resource "azurerm_storage_account" "example" {
  name = "${var.project_name}sa${random_id.suffix.hex}"
  # ... other configuration
}

State Lock Issues

Error: Error: Error acquiring the state lock

Fix: Break the lock if no other Terraform processes are running:

terraform force-unlock LOCK_ID

Provider Version Conflicts

Error: Error: Failed to query available provider packages

Fix: Clear provider cache and re-initialize:

rm -rf .terraform
rm .terraform.lock.hcl
terraform init

Common Mistakes to Avoid

Hardcoding Secrets: Never put passwords or keys directly in Terraform files. Use Azure Key Vault or environment variables:

# BAD
administrator_password = "P@ssw0rd123!"

# GOOD  
administrator_password = var.admin_password

Missing Resource Tags: Always tag resources for cost tracking and governance:

tags = {
  Environment = var.environment
  Project     = var.project_name
  ManagedBy   = "Terraform"
  CostCenter  = var.cost_center
}

Ignoring State Files: Never commit .tfstate files to version control or delete them manually.

Skipping Plan Reviews: Always run terraform plan and review changes before applying in production.

Overly Complex Configurations: Break large configurations into modules for better maintainability.

Key Takeaways

  • Service principal authentication provides secure, automated access for Terraform to manage Azure resources without human intervention
  • Remote state storage in Azure Blob Storage prevents team conflicts and provides disaster recovery for infrastructure configurations
  • Consistent resource naming and tagging strategies make multi-environment deployments manageable and cost-transparent
  • Terraform plan reviews catch potential issues before they impact production environments and help teams collaborate effectively
  • Modular configuration patterns keep complex Azure infrastructure maintainable as your cloud footprint scales
  • Environment variable management keeps sensitive credentials out of version control while maintaining deployment automation
  • State lock mechanisms prevent concurrent modifications that could corrupt infrastructure state and cause deployment failures

Next Steps

  1. Explore Terraform Modules: Learn to create reusable infrastructure components with HashiCorp's module registry
  2. Implement CI/CD Pipelines: Integrate Terraform with Azure DevOps or GitHub Actions for automated deployments
  3. Study Advanced State Management: Investigate workspace management and environment separation strategies
  4. Security Hardening: Implement Azure Key Vault integration and infrastructure scanning with tools like Checkov
  5. Cost Optimization: Set up Azure Cost Management integration and implement resource lifecycle policies

Related Articles

Thorsteinn Halldorsson Senior Cloud Engineer

Senior Cloud Engineer with 25+ years of hands-on experience across the datacenter-to-cloud stack: fiber SAN and disk storage, IBM/Lenovo blade and Dell/HP/Lenovo servers, Hyper-V and VMware clusters, and SQL and Remote Desktop Services (RDS) clusters. Deep in the Microsoft platform — Active Directory, PKI/certificate services, SQL, Power BI, Dynamics 365 Business Central (NAV) and AX (Axapta), Microsoft 365, Entra, and Intune — with a focus on Azure operations, FinOps, and applying AI tools like GitHub Copilot and Claude in real workflows. Writes practical, no-nonsense guides for IT professionals who need to ship real solutions.

Leave a Reply

Your email address will not be published. Required fields are marked *