AzureRM v4 and the subscription_id Saga
For about eighteen months, upgrading to azurerm v4 in any environment that operated above the subscription level was genuinely painful. I kept running into the same wall: Terraform code written to manage management groups, policy definitions, role assignments, and subscription vending pipelines — none of it required a subscription context to plan or validate, but the provider demanded one anyway. It was one of those breaking changes that felt like the provider team hadn't thought about the breadth of how people actually use it.
This is what happened, why it mattered especially for management group work, and what finally fixed it in v4.60.0.
What changed in v4.0
Prior to v4.0, the azurerm provider was happy with this:
provider "azurerm" {
features {}
}
The provider would pick up whatever credentials were available in the environment — az login, Workload Identity, a service principal via environment variables — and figure out the subscription from context. That worked fine for most use cases and was the standard pattern in almost every shared Terraform module out there.
In v4.0, HashiCorp made subscription_id a mandatory field in the provider block. The stated rationale was making the configuration explicit and deterministic — knowing which subscription you're operating against shouldn't be ambiguous. That's a reasonable argument for a single-subscription module. It's a terrible argument for everything above it.
The immediate fallout was that every provider "azurerm" { features {} } block everywhere needed to be updated. More importantly, it meant you couldn't authenticate via CLI without also explicitly naming the subscription — even when the operation had nothing to do with a subscription.
Why management group operations were hit hardest
The pattern I use for managing Azure governance at scale scopes everything to the management group hierarchy. Policy definitions live at the root management group. Role assignments get applied at intermediate management groups. Subscription vending logic runs at the management group level before a subscription even exists.
None of that requires a subscription_id. The resources don't live in a subscription. When I authenticate with a service principal that has rights at the management group level, asking it to supply a subscription ID is nonsensical — there isn't one, and there shouldn't be one.
Before v4.60.0, the workaround I kept landing on was this:
provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
}
That dummy GUID. In production Terraform. Committed to repositories. Because the provider insisted on having a value even when it was never going to use it.
The other option was injecting ARM_SUBSCRIPTION_ID as a dummy environment variable in every pipeline that ran management group code. I've inherited environments with this scattered across dozens of GitHub Actions workflows — nobody could remember why it was there and nobody dared remove it.
The validate bug that made things worse
When v4.0 dropped, it wasn't just terraform plan and terraform apply that required subscription_id. The enforcement also applied to terraform validate — a purely syntactic check that makes no API calls and doesn't need any credentials at all.
This broke CI pipelines that ran terraform validate as a cheap first-pass check on every commit. Engineers who didn't have subscription access — and intentionally so, because they were relying on automation for actual applies — couldn't even validate their changes locally or in a PR check.
This got fixed quickly in v4.0.1 (PR #27178). terraform validate no longer requires subscription_id. But the broader problem — that CLI auth didn't fall back to the default subscription — stayed unresolved for much longer.
The fix: v4.60.0
PR #30251 finally landed in v4.60.0 (merged February 2026, roughly seven months after it was opened).
The change is simple: when using Azure CLI authentication and subscription_id is not explicitly set in the provider block, the provider now reads the default subscription from az account show and uses that. The same behaviour everyone assumed it had in v3.
For management group operations specifically, this actually goes further than restoring the old behaviour. I can now run a pipeline that authenticates via Workload Identity against an Entra ID app with management group scope, and if no subscription is needed for the operations in that root module, I don't need to provide one. The provider figures it out from the CLI context, or doesn't need it at all.
The minimal provider block works again:
provider "azurerm" {
features {}
}
And for modules that genuinely do target a specific subscription, the explicit form is still the right approach:
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
How I cleaned this up
After pinning to v4.60.0 I went through and removed every dummy ARM_SUBSCRIPTION_ID=00000000-0000-0000-0000-000000000000 from management group pipelines. In most of them it had been there since the initial v4 migration with a comment like # required by azurerm v4, see issue #27154.
For modules that genuinely need a subscription ID I kept the explicit subscription_id — that's actually good practice, it makes the intent obvious. But anything scoped to the management group level now runs clean.
The version constraint I'm standardising on:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.60.0"
}
}
}
Things I've gotten wrong
Confusing the validate fix with the full fix. When v4.0.1 came out and fixed terraform validate, I assumed the whole problem was resolved. It wasn't. The validate bug and the "CLI auth requires subscription_id" bug were separate issues resolved ~18 months apart. I wasted time trying to pin to v4.0.1+ thinking that would be enough.
Assuming the dummy GUID was harmless. It mostly was, until we had a module that actually read back the subscription ID from the provider configuration to construct resource IDs. That broke in a confusing way and took longer to debug than it should have.
Using subscription_id = "" instead of the dummy GUID. An empty string causes a different error than a missing value. The dummy GUID 00000000-0000-0000-0000-000000000000 is what the provider would actually accept as a placeholder.