Terraform for Azure Infrastructure — A Practical Primer
Terraform for Azure Infrastructure
Section titled “Terraform for Azure Infrastructure”Infrastructure as Code isn’t about writing YAML for fun — it’s about making your infrastructure reviewable, repeatable, and recoverable. Terraform is the industry standard for multi-cloud IaC, and it pairs well with Azure.
Why Terraform Over ARM/Bicep?
Section titled “Why Terraform Over ARM/Bicep?”- Multi-cloud: One language for Azure, AWS, and everything else
- State management: Terraform tracks what exists vs. what you declared
- Plan before apply:
terraform planshows exactly what will change before anything happens - Ecosystem: Thousands of providers and modules
That said, Bicep is excellent if you’re Azure-only. Terraform wins when you span clouds or want portable skills.
| Feature | Terraform | Bicep | ARM JSON |
|---|---|---|---|
| Language | HCL | Bicep DSL | JSON |
| Multi-cloud | ✅ Yes | ❌ Azure only | ❌ Azure only |
| State management | Explicit (.tfstate) | None (Azure handles it) | None (Azure handles it) |
| Plan preview | terraform plan | az deployment what-if | az deployment what-if |
| Maturity | Very mature | Mature | Legacy |
| Learning curve | Medium | Low (for Azure devs) | High |
Provider Setup
Section titled “Provider Setup”terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } }
backend "azurerm" { resource_group_name = "rg-terraform-state" storage_account_name = "stterraformstate" container_name = "tfstate" key = "prod.terraform.tfstate" }}
provider "azurerm" { features {}}Golden rule: Never store state locally for anything beyond experiments. Use a remote backend from day one.
Common Azure Resources
Section titled “Common Azure Resources”Resource Group + App Service
Section titled “Resource Group + App Service”resource "azurerm_resource_group" "main" { name = "rg-myapp-prod" location = "Australia East"}
resource "azurerm_service_plan" "main" { name = "asp-myapp-prod" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location os_type = "Linux" sku_name = "B1"}
resource "azurerm_linux_web_app" "main" { name = "app-myapp-prod" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location service_plan_id = azurerm_service_plan.main.id
site_config { application_stack { dotnet_version = "9.0" } }}AKS Cluster (Minimal)
Section titled “AKS Cluster (Minimal)”resource "azurerm_kubernetes_cluster" "main" { name = "aks-myapp-prod" location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name dns_prefix = "myapp"
default_node_pool { name = "default" node_count = 2 vm_size = "Standard_D2s_v3" }
identity { type = "SystemAssigned" }}Patterns That Scale
Section titled “Patterns That Scale”Use Variables and Locals
Section titled “Use Variables and Locals”variable "environment" { type = string default = "prod"}
locals { resource_prefix = "myapp-${var.environment}" common_tags = { Environment = var.environment ManagedBy = "Terraform" }}Module Structure
Section titled “Module Structure”infrastructure/├── main.tf├── variables.tf├── outputs.tf├── terraform.tfvars└── modules/ ├── networking/ ├── compute/ └── database/Modules keep things DRY. Start with a flat structure and extract modules when you see repetition across environments.
Workflow
Section titled “Workflow”terraform init # download providers, init backendterraform plan # preview changes (always review this)terraform apply # apply changesterraform destroy # tear down (careful!)In CI/CD: Run plan on PR, apply on merge to main. Store the plan file as an artifact so the apply matches exactly what was reviewed.
Common Gotchas
Section titled “Common Gotchas”- Forgetting state locking — Use blob lease (Azure) or DynamoDB (AWS). Concurrent applies without locking corrupt your state file
- Secrets in state — Sensitive values land in
.tfstatein plaintext. Encrypt your remote backend and restrict access to the storage account - Massive single state file — Split state by service boundary (networking, compute, database). Large files slow down plan/apply and increase blast radius
- Skipping
planreview — The one time you skip it is the time it deletes your database. Red-lines deserve scrutiny - No
prevent_destroy— Addlifecycle { prevent_destroy = true }to databases, storage accounts, and anything critical - Provider version drift —
~> 4.0pins to 4.x. Without pinning, a major provider bump can break your plan on nextterraform init
Terraform + Azure is a deep topic. This covers the foundation — enough to provision real infrastructure and not shoot yourself in the foot.