Getting Started with Terraform on Azure — A Practical Guide
January 2026 · 9 min readInfrastructure as Code sounds great in theory. In practice, the learning curve can be steep. Here's a practical walkthrough of how I use Terraform to manage Azure resources — from project structure to state management to the gotchas nobody warns you about.
Why Terraform (Not Bicep or ARM)
Azure has Bicep, and it's excellent if you're 100% Azure. I chose Terraform because:
- The team already knew HCL syntax from other projects
- We needed to manage some non-Azure resources (Cloudflare DNS, GitHub repos)
- The
terraform planoutput is clearer than ARM “what-if” for reviewing changes - State locking with Azure Storage backend prevents team conflicts
Project Structure
infra/
├── environments/
│ ├── staging/
│ │ ├── main.tf # Module calls with staging values
│ │ ├── backend.tf # Remote state config (Azure Storage)
│ │ └── terraform.tfvars # Staging-specific variables
│ └── production/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
├── modules/
│ ├── networking/
│ │ ├── main.tf # VNet, subnets, NSGs
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ │ ├── main.tf # VMs, scale sets
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── iot/
│ ├── main.tf # IoT Hub, DPS, storage
│ ├── variables.tf
│ └── outputs.tf
└── README.md
The key pattern: modules define reusable infrastructure components, environments compose them with different parameters. This lets staging and production share the same infrastructure logic but differ in size, SKU, and networking rules.
Remote State with Azure Storage
Storing Terraform state locally is fine for experiments, but for team work you need remote state with locking:
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate"
container_name = "tfstate"
key = "staging.tfstate"
}
}
I provisioned the storage account manually (one-time bootstrap) and enabled blob versioning for state file history. This way, if a terraform apply corrupts state, we can revert to a previous version from Azure Storage.
A Real Module: IoT Hub
# modules/iot/main.tf
resource "azurerm_iothub" "main" {
name = var.iot_hub_name
resource_group_name = var.resource_group_name
location = var.location
sku {
name = var.sku_name
capacity = var.sku_capacity
}
endpoint {
type = "AzureIotHub.StorageContainer"
name = "telemetry-archive"
connection_string = var.storage_connection_string
container_name = "telemetry"
encoding = "JSON"
batch_frequency = 300
max_chunk_size = 10485760
}
route {
name = "telemetry-to-storage"
source = "DeviceMessages"
condition = "true"
endpoint_names = ["telemetry-archive"]
enabled = true
}
tags = var.tags
}
# modules/iot/variables.tf
variable "iot_hub_name" {
type = string
description = "Name of the IoT Hub instance"
}
variable "sku_name" {
type = string
default = "S1"
}
variable "sku_capacity" {
type = number
default = 1
}
The module creates an IoT Hub with a built-in route that archives all device messages to Azure Storage as JSON. In staging, we use S1 with capacity 1; production uses S2 with capacity 4.
CI/CD Integration
We run Terraform through our Jenkins pipeline (see my Jenkins post for the full setup):
# Simplified CI flow
terraform init
terraform validate
terraform plan -out=tfplan
# Human reviews the plan output, then:
terraform apply tfplan
The -out=tfplan flag is critical — it ensures the apply step executes exactly the plan that was reviewed, not whatever the current state happens to be when someone hits “approve.”
Gotchas & Lessons
- State drift is real. Someone clicked in the Azure portal and changed an NSG rule. Next
terraform planshowed a diff we didn't expect. Solution: periodicterraform planruns in CI (even without apply) to catch drift early. - Use
lifecycle { prevent_destroy = true }on databases and storage accounts. One wrongterraform destroyshouldn't wipe production data. - Tag everything. We add
environment,project, andmanaged-by = "terraform"tags to every resource. Makes Azure Cost Management reports actually useful and helps teammates know not to edit tagged resources manually. - Start small. Don't try to Terraform your entire Azure subscription on day one. Start with one resource group, one service. Expand the module library as you gain confidence.