Terraform (azurerm) as the alternative to Bicep
When Terraform's azurerm provider is the better fit than Bicep and what changes for an agent — state instead of ARM-side deployments, provider authentication (OIDC from GitHub Actions with use_oidc, managed identity, CLI), the azurerm v5 defaults that break old configurations (no automatic resource-provider registration, subscription_id required), Entra and storage settings, and known-bad configurations with their symptoms.
markdownpatternv1MFS5-8126stage: buildtrust: officialtrust: fieldservice: armservice: terraforminterface: iacTerraform azurermTerraform vs Bicepazurerm v5resource_provider_registrations noneMissingSubscriptionRegistration TerraformARM_USE_OIDC use_oidc GitHub ActionsARM_CLIENT_ID ARM_TENANT_ID ARM_SUBSCRIPTION_IDsubscription_id required plan applyfeatures block requireduse_msiuse_clistorage_use_azureadazurerm federated credentials sequential 3.40Terraform state Azure storage
When to pick which
| Pick | When |
|---|---|
| Bicep | Azure only. You want no state file to host and lock, day-one support for new API versions (once Bicep has the types), what-if, and deployment stacks for deletion and deny settings |
Terraform azurerm | You already run Terraform, manage several clouds or SaaS providers in one plan, or want plan/apply with state-based drift detection |
Both call the same Resource Manager APIs, so the ARM error catalogues (reference/arm/errors.md, reference/arm/deployment-errors.md) and the RBAC and policy documents apply unchanged. Terraform adds its own state and provider-configuration failures.
Provider configuration that works (azurerm 5.x)
features {}block: required.subscription_id: required forplanandapply(not forvalidate). Set it, orARM_SUBSCRIPTION_ID.- Resource providers: since v5 the default
resource_provider_registrationsisnone, so nothing is registered for you. Either setresource_provider_registrations = "legacy"(the v4 behaviour) or list the providers inresource_providers_to_register, or register them once withaz provider register. - Authentication:
- From GitHub Actions:
use_oidc = true(orARM_USE_OIDC=true) withARM_CLIENT_IDandARM_TENANT_ID. The provider readsACTIONS_ID_TOKEN_REQUEST_URL/_TOKENitself, and the job needspermissions: id-token: write. The federated credential rules are the same as forazure/login(reference/entra/federated-credential-errors.md). - On Azure compute:
use_msi = true. - Locally: the Azure CLI (
use_cli, on by default). storage_use_azuread = truemakes the provider use Entra ID instead of shared keys for blob and queue data. Leave it at the defaultfalseand the provider needs storage account keys, which fails when shared-key access is disabled.
Known-bad
| Configuration | Symptom |
|---|---|
| A v4 configuration upgraded to v5 unchanged | MissingSubscriptionRegistration for providers v4 used to register |
No subscription_id anywhere | plan fails; validate passes, which hides it until CI |
use_oidc without id-token: write in the workflow | The provider can't get a GitHub token (the same failure as azure/login) |
| Several federated credentials on one user-assigned identity, provider older than 3.40.0 | 409 conflicts: older versions created them concurrently |
Role assignments with name unset and principals recreated | Duplicate or conflicting assignments (RoleAssignmentExists, RoleAssignmentUpdateNotPermitted): the same causes as in Bicep (reference/rbac/errors.md) |
Shared-key access disabled on the storage account, storage_use_azuread false | Data-plane operations on containers or queues fail with authorization errors (storage family) |
The provider's own pages are the reference for resource arguments. This hub only covers what an agent gets wrong when moving between the two tools.