Skip to content
Infrastructure as Code iac state 5 min read

azurerm Backend

The azurerm backend stores Terraform state as a blob inside an Azure Storage Account container. It is the canonical remote backend for teams building on Azure, giving you durable, versioned, encrypted state plus automatic locking — without standing up any extra infrastructure. Because locking is handled natively by Azure Blob leases, there is no separate lock table to provision (unlike the S3 backend’s DynamoDB dependency). This page walks through the storage layout, how locking works, your authentication options, and a complete worked example.

How the azurerm backend is structured

Three pieces identify where your state lives:

  • Storage account — a globally unique account that holds blob storage. State is encrypted at rest by default with Microsoft-managed keys.
  • Container — a blob container inside the account (conceptually a bucket). One container typically holds state for many workspaces or projects.
  • Key — the blob name (path) of the state file within the container, for example prod/network.tfstate.

Together these form the address of a single state file. Distinct stacks or environments should use distinct keys so their state never collides.

SettingPurposeExample
resource_group_nameResource group containing the storage accounttfstate-rg
storage_account_nameGlobally unique storage accounttfstateprod9341
container_nameBlob container holding statetfstate
keyBlob path for this stack’s stateprod/app.tfstate
use_azuread_authAuthenticate via Azure AD instead of access keystrue
subscription_idTarget subscription (often via env var)00000000-0000-0000-0000-000000000000

Locking with blob leases

Every Terraform operation that writes state — apply, state mv, import, taint — first acquires a lease on the state blob. A lease is Azure’s native, blob-level mutual-exclusion primitive: while one client holds it, no other client can write to the blob. Terraform takes the lease at the start of the operation and releases it when finished, so two engineers (or two CI runs) can never clobber each other’s state.

If a run crashes and the lease is left dangling, Terraform reports a locking error on the next operation. You can break the lease and recover with:

terraform force-unlock <LOCK_ID>

Warning: Only run force-unlock when you are certain no other Terraform process is active. Breaking a live lease can corrupt state by allowing concurrent writes.

Authentication

The backend supports several credential sources, resolved roughly in this order:

MethodWhen to useHow
Azure CLILocal developmentaz login, then nothing else needed
Managed identityCI/CD on Azure (VMs, GitHub runners in Azure)use_msi = true
Service principal + secretGeneric CI/CDARM_CLIENT_ID / ARM_CLIENT_SECRET / ARM_TENANT_ID
OIDC / workload identityGitHub Actions, GitLabuse_oidc = true
Storage access keyLegacy, simplestARM_ACCESS_KEY

Prefer Azure AD (use_azuread_auth = true) over shared storage access keys. AD auth lets you grant least-privilege RBAC (the Storage Blob Data Contributor role on the container) and avoids long-lived secrets. OpenTofu implements the same azurerm backend with identical configuration, so everything here applies to tofu as well.

Worked example

First, provision the state backing store. This bootstrap config uses local state (chicken-and-egg: you cannot store the backend’s own state in the backend before it exists):

resource "azurerm_resource_group" "tfstate" {
  name     = "tfstate-rg"
  location = "eastus"
}

resource "azurerm_storage_account" "tfstate" {
  name                            = "tfstateprod9341"
  resource_group_name             = azurerm_resource_group.tfstate.name
  location                        = azurerm_resource_group.tfstate.location
  account_tier                    = "Standard"
  account_replication_type        = "GRS"
  min_tls_version                 = "TLS1_2"
  allow_nested_items_to_be_public = false

  blob_properties {
    versioning_enabled = true
  }
}

resource "azurerm_storage_container" "tfstate" {
  name                  = "tfstate"
  storage_account_id    = azurerm_storage_account.tfstate.id
  container_access_type = "private"
}

Note versioning_enabled = true — blob versioning keeps a history of every state write, which is your safety net if state is accidentally overwritten.

Now configure a separate stack to use that container as its backend:

terraform {
  required_version = ">= 1.5"

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

  backend "azurerm" {
    resource_group_name  = "tfstate-rg"
    storage_account_name = "tfstateprod9341"
    container_name       = "tfstate"
    key                  = "prod/app.tfstate"
    use_azuread_auth     = true
  }
}

provider "azurerm" {
  features {}
}

Initialize the backend. Terraform validates access and, if you are migrating from local state, offers to copy it up:

terraform init

Output:

Initializing the backend...

Successfully configured the backend "azurerm"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Installing hashicorp/azurerm v4.20.0...
- Installed hashicorp/azurerm v4.20.0 (signed by HashiCorp)

Terraform has been successfully initialized!

A subsequent apply acquires a lease before writing:

terraform apply

Output:

Acquiring state lock. This may take a few moments...

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Releasing state lock. This may take a few moments...

Partial backend configuration

Avoid hardcoding account names per environment by supplying them at init time. Leave the backend "azurerm" {} block empty and pass values with -backend-config:

terraform init \
  -backend-config="storage_account_name=tfstateprod9341" \
  -backend-config="key=prod/app.tfstate" \
  -backend-config=backend.prod.hcl

This keeps the same root module reusable across dev, staging, and prod by varying only the backend key and account.

Best Practices

  • Enable blob versioning (and consider soft delete) on the storage account so you can recover prior state revisions.
  • Use Azure AD auth with the Storage Blob Data Contributor role instead of shared access keys.
  • Restrict network access to the storage account with private endpoints or firewall rules — state contains sensitive values in plaintext.
  • Give every stack and environment a unique key to prevent state collisions.
  • Provision the backend storage account in a separate bootstrap module with its own (often local or independently stored) state.
  • Run terraform force-unlock only as a deliberate recovery step, never as part of normal automation.
Last updated June 14, 2026
Was this helpful?