State Is the Part of Terraform That Bites

Two engineers ran terraform applyon the same configuration on a Friday afternoon. Both read the same state, made different changes and wrote it back. The second write dropped the first one's records, and the next plan cheerfully proposed creating resources that already existed.

Terraform's state file is its memory of what it manages. Lose it, corrupt it or let two runs fight over it, and Terraform stops telling the truth. Below is how to keep state remote and locked on Azure Storage, how to split it so plans stay fast, how to refactor and import without destroying anything, and why state needs the same protection as your secrets.

If your state lives somewhere shared and unlocked
  • 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

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.

Removing Resources From State, and Protecting What Is in It

Sometimes Terraform should stop managing a resource without deleting it: a database handed to another team, or a resource moving to a different configuration. Running terraform state rm does this outside code review. Since Terraform 1.7 aremoved block expresses it as configuration, so the change is planned and reviewed like any other.

removed {
  from = azurerm_storage_account.legacy
  lifecycle {
    destroy = false      # forget the resource, leave it running
  }
}

Treat the state file as sensitive data. It holds every attribute of every resource, including generated passwords and connection strings in plain text. Turn on encryption and versioning on the backend storage, limit read access to the pipeline identity and a small group of operators, and practise restoring an earlier version before you need to do it during an incident.

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