Skip to content
DECLARE OR DRIFT

Terraform on Azure

Terraform on Azure

Azure Terraform gets easier when you start with scope, state, identity, permissions, secrets, and naming. Deploy the app after those pieces are clear.

The problem

Terraform on Azure is mostly about Azure's control plane: scopes, identities, role assignments, names, and eventual consistency. Creating compute is usually the easy part.

Start by understanding where a resource lives and which identity can touch it. Azure has tenant, management group, subscription, resource group, and resource scopes. Confusing those scopes produces confusing failures.

  • Scope first, resource second.
  • Identity before secrets.
  • Remote state before team collaboration.

Vocabulary

The AzureRM provider talks to Azure Resource Manager. A resource group is usually your lifecycle boundary: things created together, reviewed together, and deleted together. A storage account plus blob container is a common Terraform remote backend.

Managed identities let Azure resources authenticate without stored passwords. Role assignments connect identities to permissions. Key Vault stores secrets and keys, but your IaC should normally reference secret locations rather than hardcode secret values.

  • Resource group: lifecycle container.
  • Storage backend: shared state ledger.
  • Managed identity: passwordless workload identity.
  • Role assignment: identity plus scope plus role.
  • Key Vault: secret boundary, not a dumping ground.

How the loop works

Bootstrap Azure in two phases. First, create the Terraform state storage using a small manual script or a separate bootstrap root. Second, configure normal roots to use that backend. This avoids the circular problem of needing state before you can manage state.

For an app, the safe order is usually resource group, naming locals, identity, Key Vault, role assignments, app hosting, diagnostics, then deployment pipeline. Terraform's graph can infer some of this, but explicit references make the dependency readable.

  • Use `azurerm` backend for remote state.
  • Use provider features intentionally; do not rely on mystery defaults.
  • Expect RBAC delays after creating role assignments.
  • Make CI run plan on pull requests and apply only after approval.

Common mistakes

Azure names are inconsistent. Some are global, some are regional, some forbid hyphens, some require lowercase, and some have length limits that collide with meaningful names. Decide a naming scheme early and encode it.

RBAC can make a correct plan fail because permissions have not propagated yet. Key Vault has multiple access models. Some resources recreate when a name or immutable field changes. A beginner should read provider docs for every unfamiliar argument before applying.

  • Storage account names are globally unique and restrictive.
  • Role assignment timing failures are common.
  • Changing immutable fields can trigger replacement.
  • Provider defaults can create public exposure if you do not set network and access rules explicitly.

Working pattern

Build an Azure app root around a small set of stable decisions: resource group, environment, region, naming prefix, managed identity, Key Vault, diagnostics, and hosting target. Keep those decisions visible instead of scattering them across many files.

Use a naming local or naming module, tag everything with owner and environment, scope roles narrowly, and make production applies run from CI rather than a laptop.

  • One backend key per root and environment.
  • One identity per workload unless sharing is intentional.
  • One place for naming decisions.
  • One reviewed plan before each production apply.

The Review

  • Can you identify the tenant, subscription, resource group, and resource scope involved?
  • Are role assignments scoped narrowly enough to survive audit?
  • Can the state backend be recovered if the app subscription is damaged?
  • Did CI separate plan, approval, and apply?
azurerm remote state readonly
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate-prod"
    storage_account_name = "sttfstateprod001"
    container_name       = "tfstate"
    key                  = "apps/api/prod.tfstate"
  }
}