Skip to content
Infrastructure as Code iac providers 4 min read

The Azure Provider

The azurerm provider lets Terraform manage resources in Microsoft Azure — virtual networks, storage accounts, AKS clusters, App Services, and hundreds more. It is one of the most heavily used providers in the registry and has a few conventions that trip up newcomers, most notably the mandatory features {} block and the way Azure groups resources. This page walks through declaring the provider, authenticating against Azure, and provisioning a small but realistic stack. Everything here works identically under OpenTofu, which consumes the same provider binary from the registry.

Declaring and pinning the provider

Like every provider, azurerm is declared in required_providers so the version is locked in your lockfile. Pin to a major version and let patch releases float — Azure ships breaking changes between majors fairly often.

terraform {
  required_version = ">= 1.5.0"

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

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

The features {} block is required even when empty — the provider will not initialize without it. It exists so you can opt into per-resource-type behaviours, such as whether deleting a resource group also purges nested resources, or whether soft-deleted Key Vaults are recovered automatically.

provider "azurerm" {
  subscription_id = var.subscription_id

  features {
    resource_group {
      prevent_deletion_if_contains_resources = true
    }
    key_vault {
      purge_soft_delete_on_destroy = false
    }
  }
}

As of provider v4, subscription_id is mandatory and is no longer inferred silently from the environment. Set it explicitly via the argument or the ARM_SUBSCRIPTION_ID environment variable, or terraform plan will fail fast.

Authentication

The provider supports several auth methods. It searches them in a fixed order, so the first one it finds wins.

MethodBest forHow it’s supplied
Azure CLILocal developmentaz login, then nothing in the config
Service principal + secretCI/CD pipelinesARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID
Service principal + certificateHardened CI/CDclient_certificate_path + password
Managed identityCode running on Azure VMs/runnersuse_msi = true
OIDC / workload identityGitHub Actions, GitLabuse_oidc = true

Azure CLI (local)

The simplest path for development is to authenticate the CLI and let the provider reuse that session. No credentials live in your Terraform files.

az login
az account set --subscription "00000000-0000-0000-0000-000000000000"
terraform init && terraform plan

Service principal (CI/CD)

For automation, create a service principal and feed its credentials through environment variables — never hard-code secrets in .tf files.

az ad sp create-for-rbac --name "tf-ci" --role Contributor \
  --scopes /subscriptions/00000000-0000-0000-0000-000000000000

export ARM_CLIENT_ID="11111111-1111-1111-1111-111111111111"
export ARM_CLIENT_SECRET="<generated-secret>"
export ARM_TENANT_ID="22222222-2222-2222-2222-222222222222"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"

With those exported, the provider block stays minimal — just features {} — because every other value is read from the environment.

Resource groups: the unit of organisation

In Azure, almost every resource lives inside a resource group, which acts as a deletion and lifecycle boundary. You declare the group once and reference its name and location from the resources you nest inside it.

resource "azurerm_resource_group" "main" {
  name     = "rg-devcraftly-prod"
  location = "West Europe"

  tags = {
    environment = "production"
    managed_by  = "terraform"
  }
}

Worked example: a storage account in a resource group

This stack creates a resource group and a globally-unique storage account inside it, then exports the primary connection string as a sensitive output. Storage account names must be 3-24 lowercase alphanumeric characters and unique across all of Azure, so a random_string suffix keeps re-runs collision-free.

resource "random_string" "suffix" {
  length  = 6
  upper   = false
  special = false
}

resource "azurerm_resource_group" "main" {
  name     = "rg-devcraftly-prod"
  location = "West Europe"
}

resource "azurerm_storage_account" "assets" {
  name                     = "stdevcraftly${random_string.suffix.result}"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  min_tls_version          = "TLS1_2"

  blob_properties {
    versioning_enabled = true
  }

  tags = {
    environment = "production"
  }
}

output "storage_connection_string" {
  value     = azurerm_storage_account.assets.primary_connection_string
  sensitive = true
}

Running terraform apply produces output along these lines:

Output:

Terraform will perform the following actions:

  # azurerm_storage_account.assets will be created
  + resource "azurerm_storage_account" "assets" {
      + account_replication_type = "LRS"
      + account_tier             = "Standard"
      + name                     = "stdevcraftlya1b2c3"
      + min_tls_version          = "TLS1_2"
    }

Plan: 3 to add, 0 to change, 0 to destroy.

azurerm_resource_group.main: Creation complete after 4s
azurerm_storage_account.assets: Creation complete after 22s

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

Outputs:

storage_connection_string = <sensitive>

Best Practices

  • Always include features {} and set subscription_id explicitly — both are required by provider v4 and omitting them fails init/plan.
  • Keep credentials out of .tf files; use az login locally and ARM_* environment variables (or OIDC) in CI.
  • Pin azurerm with ~> 4.0 and commit .terraform.lock.hcl so every run uses the same binary across Terraform and OpenTofu.
  • Group resources by lifecycle into resource groups, and reference azurerm_resource_group.<name>.location/.name rather than repeating literals.
  • Tag every resource consistently (environment, managed_by) to make cost reporting and cleanup tractable.
  • Mark connection strings, keys, and passwords as sensitive = true so they never print to the console or logs.
Last updated June 14, 2026
Was this helpful?