Terraform State Management in Azure

Terraform state is the file that connects your HCL configuration to real Azure resources. By default it lives on your laptop — which works for solo experiments but breaks immediately the moment a second team member tries to run Terraform. This page covers how to set up Azure Blob Storage as a remote state backend, enable locking, and handle state operations safely.

What the state file contains

After you run terraform apply, Terraform writes a terraform.tfstate file containing the IDs, attributes, and metadata of every resource it manages. On the next plan, Terraform reads state, queries Azure for the current configuration of each resource, and calculates what changes are needed.

A snippet of state for a resource group looks like this:

{
  "version": 4,
  "terraform_version": "1.7.4",
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "main",
      "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/xxxx/resourceGroups/myapp-dev-rg",
            "location": "eastus",
            "name": "myapp-dev-rg",
            "tags": {
              "environment": "dev",
              "managed_by": "terraform"
            }
          }
        }
      ]
    }
  ]
}
Note

The state file can contain sensitive values — storage account connection strings, database passwords, and other secrets are stored in plaintext in the state JSON. This is a known Terraform limitation. Storing state in Azure Blob Storage with encryption at rest and restricted access is essential to protect these values.

Creating the storage backend for state

The state backend needs to exist before any Terraform configuration can use it. You cannot use Terraform to create its own backend — you have to create the storage account first using the Azure CLI or the portal. This is a one-time setup per team or project.

# Set variables
RESOURCE_GROUP="terraform-state-rg"
STORAGE_ACCOUNT="tfstate$(openssl rand -hex 4)"   # random suffix for uniqueness
CONTAINER_NAME="tfstate"
LOCATION="eastus"

# Create a dedicated resource group for state
az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION

# Create the storage account
az storage account create \
  --resource-group $RESOURCE_GROUP \
  --name $STORAGE_ACCOUNT \
  --sku Standard_LRS \
  --encryption-services blob \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false

# Create the blob container
az storage container create \
  --name $CONTAINER_NAME \
  --account-name $STORAGE_ACCOUNT

# Enable versioning on the container (allows recovery from corruption)
az storage account blob-service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --enable-versioning true

# Print the storage account name to use in backend.tf
echo "Storage account name: $STORAGE_ACCOUNT"

Configuring the azurerm backend

Add a backend “azurerm” block to the terraform block in your providers.tf file. When you run terraform init with this configuration, Terraform migrates any local state to the remote backend and starts using it from that point forward.

# providers.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.90"
    }
  }

  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstate1a2b3c4d"   # your actual storage account name
    container_name       = "tfstate"
    key                  = "dev/terraform.tfstate"
  }
}

provider "azurerm" {
  features {}
}
# Initialize — migrates local state to remote if needed
terraform init

# Terraform will ask:
# "Do you want to copy existing state to the new backend?"
# Type 'yes' to migrate.

# Verify remote state is working
terraform state list

Using partial backend configuration for CI/CD

Hardcoding the storage account name in a file that lives in git is fine for private repos, but some teams prefer to pass backend values at init time. Terraform supports partial backend configuration where you leave the backend block empty and pass values via a backend config file or environment variables.

# providers.tf — empty backend block
terraform {
  backend "azurerm" {}
}
# Pass backend config at init time with a .tfbackend file
terraform init \
  -backend-config="resource_group_name=terraform-state-rg" \
  -backend-config="storage_account_name=tfstate1a2b3c4d" \
  -backend-config="container_name=tfstate" \
  -backend-config="key=dev/terraform.tfstate"

# Or with a backend config file
terraform init -backend-config=dev.tfbackend
# dev.tfbackend (do not commit if it contains secrets)
resource_group_name  = "terraform-state-rg"
storage_account_name = "tfstate1a2b3c4d"
container_name       = "tfstate"
key                  = "dev/terraform.tfstate"

State locking and what to do when a lock gets stuck

When Terraform starts an operation that modifies state (plan, apply, destroy), it acquires a lease on the blob. If the operation is interrupted — a pipeline agent crashes, a network timeout, someone kills the terminal — the lock can remain even though no Terraform process is running.

# When you see this error:
# Error: Error locking state: Error acquiring the state lock
# Lock Info:
#   ID:        xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
#   Operation: OperationTypePlan
#   Who:       user@hostname
#   Created:   2026-03-19 10:23:45

# Verify no Terraform process is actually running, then force-unlock:
terraform force-unlock xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# Double-check the state is healthy after unlocking
terraform plan
Tip

Before force-unlocking, verify that the process that acquired the lock is truly gone. If a long-running terraform apply is still in progress and you force-unlock it, two concurrent applies can corrupt state. Check your pipeline logs or ask teammates before using force-unlock.

Common state operations

Terraform provides CLI commands for managing individual entries in state. These are useful when resources drift from state — for example when someone manually deletes a resource through the portal, or when you need to move a resource between modules without destroying it.

# List all resources in state
terraform state list

# Show the state of one specific resource
terraform state show azurerm_storage_account.main

# Remove a resource from state without deleting it from Azure
# Use this when you want Terraform to stop managing a resource
terraform state rm azurerm_storage_account.main

# Import an existing Azure resource into state
# Use this when a resource was created outside Terraform and you want to manage it
terraform import azurerm_storage_account.main \
  /subscriptions/xxxx/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorage

# Move a resource within state (useful after refactoring module structure)
terraform state mv \
  azurerm_storage_account.main \
  module.storage.azurerm_storage_account.main

# Pull a copy of remote state to stdout (useful for inspection)
terraform state pull | jq '.resources[].type' | sort | uniq

Real scenario: recovering from a manual deletion

A common problem: someone manually deletes a resource group through the Azure portal. The next time your pipeline runs terraform plan, it reports many resources to be added — because state still shows them existing but they are gone.

The safest recovery path:

# Step 1 — Run plan to see exactly what Terraform thinks needs to be recreated
terraform plan -out=recovery.tfplan

# Step 2 — Review the plan carefully
terraform show recovery.tfplan

# Step 3 — If the plan looks correct (recreate everything that was deleted),
# apply it. Terraform will recreate all the missing resources.
terraform apply recovery.tfplan

# If some resources still exist and Terraform wrongly wants to recreate them,
# import them back into state first:
terraform import azurerm_resource_group.main \
  /subscriptions/xxxx/resourceGroups/myapp-dev-rg

Common mistakes

  1. Not enabling blob versioning on the state container. Without versioning, a corrupted state file has no recovery path. Blob versioning in Azure Storage preserves previous versions of the state blob so you can restore a known-good state. Enable it at storage account creation time.
  2. Sharing one state file across environments. If dev and prod state are in the same blob, a terraform destroy in dev that accidentally targets the wrong workspace can affect production resources. Always use separate state keys (or separate storage containers) per environment.
  3. Editing the state file manually. The state file is JSON and it is tempting to edit it in a text editor when something is wrong. Manual edits break the state checksums and can cause irreversible corruption. Always use terraform state rm, terraform state mv, and terraform import instead.

Frequently asked questions

What is Terraform state and why does it matter?

Terraform state is a JSON file that records what resources Terraform has deployed and their current configuration. Without state, Terraform cannot know whether a resource already exists or needs to be created. The state file is the source of truth that allows Terraform to plan incremental changes rather than recreating everything on each run.

What happens if two people run terraform apply at the same time?

If state is stored in Azure Blob Storage, Terraform uses blob lease locking to prevent concurrent applies. The second person will get an error saying the state is locked. They must wait for the first apply to finish before running their own. If state is stored locally, both applies run simultaneously and the last one to write wins — almost certainly corrupting state.

My Terraform state is corrupted or shows resources that no longer exist. How do I fix it?

Use terraform state rm to remove specific resources from state without destroying them, or terraform import to bring existing resources under Terraform management. For severe corruption, you may need to restore from a blob versioned backup. Always enable versioning on your state storage container.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.