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.
| Setting | Purpose | Example |
|---|---|---|
resource_group_name | Resource group containing the storage account | tfstate-rg |
storage_account_name | Globally unique storage account | tfstateprod9341 |
container_name | Blob container holding state | tfstate |
key | Blob path for this stack’s state | prod/app.tfstate |
use_azuread_auth | Authenticate via Azure AD instead of access keys | true |
subscription_id | Target 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-unlockwhen 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:
| Method | When to use | How |
|---|---|---|
| Azure CLI | Local development | az login, then nothing else needed |
| Managed identity | CI/CD on Azure (VMs, GitHub runners in Azure) | use_msi = true |
| Service principal + secret | Generic CI/CD | ARM_CLIENT_ID / ARM_CLIENT_SECRET / ARM_TENANT_ID |
| OIDC / workload identity | GitHub Actions, GitLab | use_oidc = true |
| Storage access key | Legacy, simplest | ARM_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 Contributorrole 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
keyto 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-unlockonly as a deliberate recovery step, never as part of normal automation.