Infrastructure as Code (IaC) has become the backbone of modern DevOps practices, and Terraform is one of the most popular tools for turning declarative code into real cloud resources. If you’ve already spun up a few virtual machines manually on Azure, you know how tedious and error‑prone the process can become. This guide walks you through automating Azure infrastructure provisioning with Terraform, from installing the CLI to managing state securely. By the end, you’ll have a reproducible, version‑controlled Azure environment you can spin up or tear down with a single command.
What You’ll Need
- Azure subscription (owner or contributor role)
- Terraform installed (v1.5+ recommended)
- Azure CLI installed and logged in
- Git for version control
- Basic knowledge of Azure services (Resource Groups, Virtual Networks, etc.)
Step 1: Install Terraform and Authenticate to Azure
First, download the Terraform binary for your OS from HashiCorp’s site. Unzip the file and move the executable to a directory in your PATH.
“`bash
# macOS example using Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Verify installation
terraform version
“`
Next, install the Azure CLI (az) and sign in:
“`bash
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Debian/Ubuntu example
az login
“`
The CLI will open a browser window for authentication. Once logged in, you can verify your subscription:
“`bash
az account show
“`
Terraform can now use the Azure CLI’s token to authenticate, which is perfect for local development.
Step 2: Create a New Terraform Project
Choose a folder for your project and initialise a Git repository so you can track changes.
“`bash
mkdir terraform-azure-demo && cd terraform-azure-demo
git init
“`
Create a main.tf file that will hold the Azure provider configuration and the first resource – a resource group.
“`hcl
terraform {
required_version = “>= 1.5”
required_providers {
azurerm = {
source = “hashicorp/azurerm”
version = “>= 3.0”
}
}
backend “local” {}
}
provider “azurerm” {
features {}
}
resource “azurerm_resource_group” “demo_rg” {
name = “tf-demo-rg”
location = “East US”
}
“`
Run terraform init to download the Azure provider and initialise the working directory.
“`bash
tf init
“`
You should see a success message indicating the provider is ready.
Step 3: Define Core Infrastructure (VNet, Subnet, and NSG)
Now expand main.tf to include a virtual network, a subnet, and a network security group (NSG) that will later protect a virtual machine.
“`hcl
resource “azurerm_virtual_network” “demo_vnet” {
name = “tf-demo-vnet”
address_space = [“10.0.0.0/16”]
location = azurerm_resource_group.demo_rg.location
resource_group_name = azurerm_resource_group.demo_rg.name
}
resource “azurerm_subnet” “demo_subnet” {
name = “tf-demo-subnet”
resource_group_name = azurerm_resource_group.demo_rg.name
virtual_network_name = azurerm_virtual_network.demo_vnet.name
address_prefixes = [“10.0.1.0/24”]
}
resource “azurerm_network_security_group” “demo_nsg” {
name = “tf-demo-nsg”
location = azurerm_resource_group.demo_rg.location
resource_group_name = azurerm_resource_group.demo_rg.name
security_rule {
name = “AllowSSH”
priority = 1000
direction = “Inbound”
access = “Allow”
protocol = “Tcp”
source_port_range = “*”
destination_port_range = “22”
source_address_prefix = “*”
destination_address_prefix = “*”
}
}
“`
Save the file, then run a plan to see what Terraform will create:
“`bash
tf plan -out=tfplan.out
“`
If the plan looks good, apply it:
“`bash
tf apply “tfplan.out”
“`
Terraform will ask for confirmation; type yes.
Step 4: Provision a Linux Virtual Machine
With networking in place, add a Linux VM resource. We’ll use a simple Ubuntu image and a generated SSH key pair.
“`hcl
resource “tls_private_key” “ssh_key” {
algorithm = “RSA”
rsa_bits = 4096
}
resource “azurerm_linux_virtual_machine” “demo_vm” {
name = “tf-demo-vm”
resource_group_name = azurerm_resource_group.demo_rg.name
location = azurerm_resource_group.demo_rg.location
size = “Standard_B1s”
admin_username = “azureuser”
network_interface_ids = [azurerm_network_interface.demo_nic.id]
admin_ssh_key {
username = “azureuser”
public_key = tls_private_key.ssh_key.public_key_openssh
}
os_disk {
caching = “ReadWrite”
storage_account_type = “Standard_LRS”
}
source_image_reference {
publisher = “Canonical”
offer = “UbuntuServer”
sku = “22_04-lts-gen2”
version = “latest”
}
}
resource “azurerm_network_interface” “demo_nic” {
name = “tf-demo-nic”
location = azurerm_resource_group.demo_rg.location
resource_group_name = azurerm_resource_group.demo_rg.name
ip_configuration {
name = “internal”
subnet_id = azurerm_subnet.demo_subnet.id
private_ip_address_allocation = “Dynamic”
public_ip_address_id = azurerm_public_ip.demo_pip.id
}
}
resource “azurerm_public_ip” “demo_pip” {
name = “tf-demo-pip”
location = azurerm_resource_group.demo_rg.location
resource_group_name = azurerm_resource_group.demo_rg.name
allocation_method = “Dynamic”
}
“`
Run terraform apply again. After the apply completes, you can retrieve the public IP address:
“`bash
tf output -raw azurerm_public_ip.demo_pip.ip_address
“`
Use the generated private key to SSH into the VM:
“`bash
ssh -i $(terraform output -raw tls_private_key.ssh_key.private_key_pem) azureuser@
“`
You now have a fully provisioned, reachable VM managed by Terraform.
Step 5: Store Terraform State Remotely with Azure Storage
Local state works for quick experiments, but in a team environment you need a remote backend to avoid state corruption. Create a storage account and a container that Terraform will use.
“`bash
az storage account create
–name tfstate$(date +%s)
–resource-group tf-demo-rg
–location eastus
–sku Standard_LRS
az storage container create
–account-name tfstate$(date +%s)
–name tfstate
“`
Update the terraform block in main.tf to reference the AzureRM backend:
“`hcl
tterraform {
backend “azurerm” {
resource_group_name = “tf-demo-rg”
storage_account_name = “tfstate123456” # replace with your account name
container_name = “tfstate”
key = “terraform.tfstate”
}
}
“`
Run terraform init -reconfigure to migrate the existing local state to the new backend.
“`bash
tf init -reconfigure
“`
Terraform will prompt to copy the state; answer yes. From now on, the state file lives securely in Azure Storage, and you can enable state locking automatically.
Step 6: Modularise the Code for Reuse
As your infrastructure grows, keep the codebase tidy by extracting reusable blocks into modules. Create a modules/vnet folder with its own main.tf, variables.tf, and outputs.tf.
“`hcl
# modules/vnet/main.tf
resource “azurerm_virtual_network” “this” {
name = var.name
address_space = var.address_space
location = var.location
resource_group_name = var.resource_group_name
}
resource “azurerm_subnet” “this” {
name = var.subnet_name
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.this.name
address_prefixes = var.subnet_prefixes
}
“`
“`hcl
# modules/vnet/variables.tf
variable “name” {}
variable “address_space” { type = list(string) }
variable “location” {}
variable “resource_group_name” {}
variable “subnet_name” {}
variable “subnet_prefixes” { type = list(string) }
“`
“`hcl
# modules/vnet/outputs.tf
output “vnet_id” { value = azurerm_virtual_network.this.id }
output “subnet_id” { value = azurerm_subnet.this.id }
“`
Back in the root module, call the new VNet module:
“`hcl
module “vnet” {
source = “./modules/vnet”
name = “tf-demo-vnet”
address_space = [“10.0.0.0/16”]
location = azurerm_resource_group.demo_rg.location
resource_group_name = azurerm_resource_group.demo_rg.name
subnet_name = “tf-demo-subnet”
subnet_prefixes = [“10.0.1.0/24”]
}
“`
Modules make it trivial to spin up identical environments (dev, test, prod) by just changing input variables. Commit the module folder to your repo, and you’ll have a clean, maintainable codebase.
Common Mistakes to Avoid
1. Hard‑coding credentials. Storing client secrets or access keys in *.tf files leads to leaks. Use Azure AD service principals and reference them via environment variables or a .tfvars file that is excluded from Git.
2. Ignoring state locking. When multiple people run terraform apply against the same state file without a lock, you can corrupt the state. The AzureRM backend provides automatic locking, but only if you configure it correctly.
3. Over‑provisioning resources. New Terraform users often copy‑paste example blocks without adjusting sizes. Remember that Azure charges per VM size and per storage GB. Use terraform plan to double‑check before applying.
4. Forgetting to run terraform fmt and terraform validate. Clean, validated code prevents syntax errors that would otherwise break a pipeline.
5. Not version‑pinning provider plugins. The required_providers block should specify exact versions. Upgrading providers without testing can cause subtle resource drift.
Tips and Tricks
• Use Workspaces for environments. terraform workspace new dev creates an isolated state for a dev environment while sharing the same configuration.
• Leverage Data Sources. Pull existing Azure resources (like a pre‑existing VNet) with data "azurerm_virtual_network" to avoid duplicate creation.
• Enable Remote Execution in CI/CD. Store the Terraform binary in your pipeline image, run terraform init with a backend, and use terraform plan -out=plan.out followed by terraform apply plan.out for safe deployments.
• Tag Resources Consistently. Add a tags block to every resource; it helps with cost tracking and governance.
• Use the Azure Resource Manager (ARM) Template Exporter. If you have existing resources, you can export them to an ARM template and then use terraformer to generate Terraform code automatically.
Frequently Asked Questions
Do I need an Azure service principal for Terraform?
No, the Azure CLI authentication works for local development, but a service principal is recommended for CI/CD pipelines and for non‑interactive runs.
How can I protect my Terraform state from accidental deletion?
Store state in an Azure Storage account with soft‑delete enabled and use role‑based access control (RBAC) to limit who can read or write the state file.
Can I use Terraform to manage Azure AD resources?
Yes. The azuread provider lets you create groups, applications, and service principals. Just add a separate provider block and keep the permissions scoped appropriately.
Conclusion
Automating Azure infrastructure with Terraform brings repeatability, version control, and collaboration to your cloud operations. By following the steps above—installing the CLI, writing declarative HCL, storing state remotely, and modularising your code—you’ll be able to spin up robust Azure environments in minutes rather than hours. Remember to guard your credentials, lock your state, and keep your modules tidy. With these practices in place, Terraform becomes a powerful ally in your DevOps toolkit, letting you focus on delivering value instead of manually clicking through the Azure portal.
Photo by Portraits of Modern Dairy on Unsplash





