Problem Context

Terraform's state file is the mapping between your configuration and the real infrastructure. By default it's a local file โ€” terraform.tfstateโ€” sitting in your working directory. This works for solo experimentation. In a team environment, it's a disaster: two engineers run terraform apply simultaneously and corrupt state, someone deploys from a branch with stale state and deletes resources, and the file gets committed to Git exposing sensitive outputs.

Remote state backends move the state file to a centrally managed, locked, versioned store. State locking prevents concurrent modifications. Together they make Terraform safe for team use.

๐Ÿค” Sound familiar?
  • Two teammates ran terraform apply at the same time and ended up with a corrupted state file
  • State was accidentally committed to Git โ€” now it contains secrets in your history
  • Someone ran Terraform from a local machine with outdated state and deleted a production database
  • You've never thought about state backup โ€” and now it's gone

Remote state with locking is not optional for production โ€” it's the baseline.

Azure Storage Backend

The most common remote backend for Azure-hosted infrastructure is Azure Blob Storage with Azure-native state locking:

Create the backend infrastructure (one-time setup)

# Create storage account for Terraform state
RESOURCE_GROUP="rg-terraform-state"
STORAGE_ACCOUNT="stterraformstate001"
CONTAINER="tfstate"
LOCATION="westus2"

az group create --name $RESOURCE_GROUP --location $LOCATION

az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --sku Standard_LRS \
  --allow-blob-public-access false \
  --min-tls-version TLS1_2

az storage container create \
  --name $CONTAINER \
  --account-name $STORAGE_ACCOUNT

# Enable versioning for state recovery
az storage account blob-service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --enable-versioning true

Configure the backend in Terraform

# backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstate001"
    container_name       = "tfstate"
    key                  = "production/myapp.tfstate"
    use_azuread_auth     = true   # Use Azure AD instead of storage key
  }
}
# Initialize with the backend
terraform init

# If migrating from local state
terraform init -migrate-state

State Locking

Azure Blob Storage backends use blob lease locking. When terraform apply starts, it acquires a lease on the state blob. Any concurrent operation from another process fails with a lock error. The lock is released when the operation completes (or forcibly broken with terraform force-unlock if a process crashed mid-run).

# List current locks (Terraform Cloud/Enterprise) or check blob lease via CLI
az storage blob show \
  --account-name stterraformstate001 \
  --container-name tfstate \
  --name production/myapp.tfstate \
  --query properties.lease

# Force-unlock if a process crashed (use with caution)
terraform force-unlock LOCK_ID

State File Organization

For multi-environment, multi-team deployments, use separate state files per environment and component:

# State key structure
tfstate/
โ”œโ”€โ”€ shared/networking.tfstate         # Shared VNet, DNS
โ”œโ”€โ”€ dev/app.tfstate                   # App layer, dev
โ”œโ”€โ”€ dev/database.tfstate              # Data layer, dev
โ”œโ”€โ”€ staging/app.tfstate
โ”œโ”€โ”€ staging/database.tfstate
โ”œโ”€โ”€ production/app.tfstate
โ””โ”€โ”€ production/database.tfstate
# Use workspaces OR separate backends โ€” not both
# Recommended: separate backend keys per environment

# backend config per environment (passed via -backend-config)
terraform init \
  -backend-config="key=production/app.tfstate" \
  -backend-config="resource_group_name=rg-terraform-state"

# Or use partial backend configuration files
# backends/prod.hcl
resource_group_name  = "rg-terraform-state"
storage_account_name = "stterraformstate001"
container_name       = "tfstate"
key                  = "production/app.tfstate"

terraform init -backend-config=backends/prod.hcl

Importing Existing Resources into State

When you adopt Terraform for resources that already exist, you need to import them into state rather than recreating them:

# Classic import (Terraform < 1.5)
terraform import azurerm_resource_group.main \
  /subscriptions/sub-id/resourceGroups/rg-prod

# Modern import block (Terraform >= 1.5 โ€” generates config too)
import {
  to = azurerm_resource_group.main
  id = "/subscriptions/sub-id/resourceGroups/rg-prod"
}

# Generate config from import
terraform plan -generate-config-out=generated.tf

Moved Blocks: Refactoring Without Destroy/Recreate

# When you rename a resource in config, Terraform thinks it's a new resource
# Use 'moved' blocks to tell Terraform about the rename:

moved {
  from = azurerm_storage_account.old_name
  to   = azurerm_storage_account.new_name
}

# Terraform will update state without destroying/recreating the resource

State Backup and Disaster Recovery

# Manual backup before risky operations
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate

# List all resources in state
terraform state list

# Show state for a specific resource
terraform state show azurerm_storage_account.main

# Remove a resource from state (without destroying it)
terraform state rm azurerm_storage_account.orphaned

# Restore from a backup
terraform state push backup-20260801-143000.tfstate

With Azure Blob versioning enabled, you can restore previous versions of the state file through the portal or CLI without needing manual backups.

Production Checklist

  • Always use remote state โ€” never commit .tfstate to Git
  • Enable blob versioning on the state storage account for recovery
  • Use use_azuread_auth = true for the backend โ€” no storage account keys in CI
  • Separate state files per environment; don't use workspaces as an environment strategy
  • Back up state before any destructive refactoring with terraform state pull
  • Use movedblocks when renaming resources โ€” don't destroy and recreate
  • Restrict write access to state storage; developers need read for plan, write only for apply