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.
- Two teammates ran
terraform applyat 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 trueConfigure 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-stateState 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_IDState 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.hclImporting 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.tfMoved 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 resourceState 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.tfstateWith 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
.tfstateto Git - Enable blob versioning on the state storage account for recovery
- Use
use_azuread_auth = truefor 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

